Linux 网络编程笔记
网络基础
网络的诞生是为了实现独立计算机之间的互联,将多台不同且独立的计算机连接在一起。
局域网(LAN):通过交换机和路由器在有限区域内连接多台计算机。
广域网(WAN):连接远距离的计算机,通常是多个局域网的集合。
局域网基础
每层协议都会包含自己的报头,用于标识协议类型。数据包在传输过程中会包含:
报文 = 报头 + 有效载荷
设备若要实现跨网络的数据转发,至少要连接两个网络。
网络协议
协议:一组约定,用于不同厂商的计算机之间顺畅通信。通过网络协议的标准化,各种设备能够遵循统一的标准进行交流。
协议分层
OSI 七层模型
OSI(开放系统互连)模型定义了七层网络协议,用于不同主机类型之间的数据传输:
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
TCP/IP 模型
TCP/IP模型是应用广泛的简化模型,适用于实际的网络编程:
物理层:负责通过物理介质(如光纤、Wi-Fi)传递信号。
数据链路层:负责数据帧的传送和设备识别。
网络层:管理地址和路由选择,例如通过路由器建立通信路径。
传输层:确保两台主机之间的可靠数据传输(如TCP协议)。
应用层:网络编程主要在应用层进行。
网络传输基本流程
主机 A 的传输流程
应用层(A) 主机 A 的应用层生成数据(文件内容)并打包为应用层数据包,附加应用层协议的头信息。
传输层(A) 应用层数据包传递到传输层(如 TCP 协议),进行分段处理并加上传输层头信息(如端口号、校验和)。每个分段被封装为一个数据段。
网络层(A) 数据段进入网络层,加上网络层头部(如 IP 地址),形成数据包,标明源 IP 和目标 IP(主机 A 和主机 B 的地址)。
数据链路层(A) 网络层数据包进入数据链路层,为数据帧附加数据链路层头部和尾部(如 MAC 地址),包含源 MAC 和目标 MAC 地址信息。此时,数据包被封装为数据帧,准备在物理介质上传输。
网络令牌环机制
在网络令牌环协议中,网络上存在一个称为“令牌”的特殊帧,顺时针或逆时针在各节点间传递。只有持有令牌的节点才有权限发送数据。
主机 A 检测到令牌到达时,抓取令牌并持有令牌进行数据传输。持有令牌后,主机 A 开始向主机 B 发送数据帧。
数据传输完成后,主机 A 将令牌释放,使其重新在网络上流通,以便其他节点获取令牌并发送数据。
物理层
数据链路层的数据帧传输到物理层,转换为电信号或光信号,通过物理介质(如网线、光纤)传输。
其中网络令牌类似
主机 B 的接收流程
物理层(B) 主机 B 的物理层接收信号并将其转换为数据帧。
数据链路层(B) 数据帧传递至数据链路层,进行错误检测,并通过 MAC 地址验证帧的目的地址是否为主机 B。如果符合,数据帧解封装为数据包并传递至上层。
网络层(B) 网络层解封装 IP 头部,确认目的 IP 地址,并将数据包传递给传输层。
传输层(B) 传输层进行重组、校验,确认数据完整性后,将数据传递至应用层。
应用层(B) 最终,数据在应用层解封装,主机 B 的应用程序接收并处理该数据。
数据传输步骤
发送过程:数据在每一层中会被封装上一个报头。
接收过程:数据逐层去除报头(解封装),传递至对应协议。
数据处理的关键点:
如何区分报头和有效载荷。
确保有效载荷传递到每一层的正确协议。
网络中的地址
IP 地址
两种主要协议:IPv4 和 IPv6。
IPv4:多用于内网和互联网通信,使用4字节(32位)整数表示,以点分十进制格式(如192.168.1.1)。
地址范围:
[1.1.1.1 - 254.254.254.254]
,其中255保留为广播地址。
MAC 地址
MAC 地址:在数据链路层用于识别设备的唯一标识符,通常与网卡绑定。
格式:48位,使用十六进制表示,如
00:1A:2B:3C:4D:5E
。MAC 地址全球唯一,但虚拟机可能会使用虚拟的MAC地址。
TCP 和 UDP
TCP是可靠传输协议,提供面向连接的、可靠的、按序传递的数据传输。
UDP是不可靠传输协议,是无连接的,不保证数据顺序和完整性。
IP 和 端口号
IP地址:用于唯一标识网络中的一台主机。
端口号:用于唯一标识该主机上服务进程。
IP + 端口:唯一标识一台主机上的服务进程(如IPA + portA标识主机A上的特定进程)。
网络通信的本质
本质上是进程间的通信。
进程不一定都提供网络服务或请求,但提供网络服务的进程使用IP + 端口来唯一标识。
客户端到服务器通信中,客户端除了数据还需发送自己的IP和端口给服务器。
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分.
在网络传输中大端 和小端 传输的数据都默认转化成大端进行网络通讯
大端和小端的定义
大端字节序 (Big-endian):高字节在低地址,低字节在高地址。大端格式类似于人们书写数字的方式(从左到右),例如数值
0x1234
在内存中表示为12 34
(低地址在左,高地址在右)。小端字节序 (Little-endian):低字节在低地址,高字节在高地址。例如
0x1234
在小端存储时,低地址保存34
,高地址保存12
。
TCP/IP协议规定,网络数据流要采用大端字节序,即低地址存高字节。
数据先发出的部分存放在低地址,后发出的在高地址。
如果发送主机是小端机器(内存低地址存低字节),就需要将数据转换成大端格式;如果是大端机器,则可直接发送。
OS 网络编程接口
sockaddr
结构体
在Socket API中,sockaddr
结构提供了一种抽象的网络编程接口,适用于多种底层网络协议(如IPv4、IPv6和UNIX Domain Socket)。虽然每种网络协议的地址格式不同,但Socket API使用struct sockaddr *
类型的指针表示通用地址。
sockaddr
的使用
在具体使用时,
sockaddr
指针通常强制转换为协议特定的地址结构,例如sockaddr_in
(用于IPv4)或sockaddr_in6
(用于IPv6),以便访问协议对应的地址字段。这种设计增强了程序的通用性,使同一套代码可以处理不同的网络协议,例如在传参时可以接收IPv4、IPv6或UNIX Domain Socket的
sockaddr
结构体指针。
int socket(int domain, int type, int protocol);
<sys/socket.h>
参数:
domain 指定通讯领域
AF_INET : IPV4 网络协议域
AF_INET6: IPV6 网络协议域
AF_UNIX : UNIX 套接字 (用于本进程间通信)
AF_LOCAL:AF_UNIX 相同,UNIX 域套接字
AF_PACKET:与网络设备驱动程序直接通信的原始套接字
type 套接字的通信类型
SOCK_STREAM:提供顺序、可靠、双向连接的基于字节的流(TCP)。
SOCK_DGRAM:提供无连接的、不可靠的数据报服务(UDP)。
SCOK_ROW : 原始套接字,访问较低层次的协议
protocol 指定了使用的特定协议 (0 系统自动选择合适的协议)
SOCK_STREAM
IPPROTO_TCP
(TCP 协议)SOCK_DGRAM
IPPROTO_UDP
(UDP 协议)
返回值 : 成功返回 新的文件描述符 失败返回-1
uint16_t htons(uint16_t hostshort);
#include <arpa/inet.h>
参数:
传递一个 uint16_t hostshort 一个端口号
返回一个用于在网络上传输的端口号 传递给(xxx.sin_port)
in_addr_t inet_addr(const char *cp);
SYNOPSIS #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
参数
传递一个
const char *
的IP地址
返回值:
成功则返回一个无符号证书 网络字节序 返回值是大端序的IPv4 地址
如果不是有效的IP 返回
0xffffffff
,表示转化失败
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在网络编程中用于将一个套接字(socket)与一个特定的网络地址(包括IP地址和端口号)关联起来。这个函数允许套接字监听特定的端口,以便能够接收发送到该端口的数据。
#include <sys/socket.h>
参数
sockfd
套接字文件描述符,即通过
socket
函数创建的套接字。addr : 指向一个
sockaddr
结构体的指针对于IPv4,这通常是
sockaddr_in
结构体;对于IPv6,则是sockaddr_in6
结构体addrlen : 指向地址结构体的大小
ssize_t recvfrom()
(int socket, void restrict buffer, size_t length,int flags, struct sockaddr restrict address,socklen_t *restrict address_len);
#include <sys/socket.h>
receive a message from a socket 从网络接收消息
struct sockaddr_in
定义结构体变量 xxx
,用于存储发送方的地址信息
参数
socket 网络文件描述符fd
buffer 缓冲区
bufferlength
flags 指定接收操作的标志位。通常设置为
0
addres:
struct sockaddr *restrict
sockaddr
结构体的指针,用于存储发送方的地址信息。addreslen : 该变量在调用时应该被初始化为指向的
address
结构体的大小.
返回值:
成功时,返回实际接收到的字节数。
失败时,返回
-1
,并设置errno
以指示错误原因
[[deprecated]] char *inet_ntoa(struct in_addr in);
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
将网络字节序列转换成IPv4地址
ssize_t sendto()
#include <sys/socket.h>
ssize_t sendto(int socket, const void message, size_t length, int flags, const struct sockaddr dest_addr, socklen_t dest_len);
sendto — send a message on a socket
参数
socket 网络套接字
const void *message
指向要发送的缓冲区指针size_t length
指向要发送数据的长度发送数据的标致
.....
const struct sockaddr *dest_addr
指向目的地址的sockaddr
结构体的指针
返回值
成功时,返回发送的字节数。
失败时,返回
-1
并设置errno
以指示错误。
FILE popen(const char command, const char *type);
pipe+fork+exec* = popen
pipe()
int pipe(int filedes[2]);
filedes[0]
是读端文件描述符。filedes[1]
是写端文件描述符。
fork()
fork
系统调用创建一个新的进程
exec*
exec*
系列函数用于在当前进程中执行一个新的程序。当调用exec*
函数时,当前进程的地址空间会被新程序的地址空间替换,但进程ID不变。
popen()
FILE *popen(const char *command, const char *type);
command
是要执行的命令字符串。type
指定打开模式,可以是"r"
(读)或"w"
(写)。
它创建一个管道,调用fork
,然后在子进程中调用exec*
来执行指定的命令。父进程可以通过返回的文件流与子进程通信。
返回值是一个指向FILE
流的指针,父进程可以通过这个流读取子进程的输出或将输入发送给子进程。
TCP Server
在udp 写好后 是 可以直接连接的而 Tcp需要建立连接
Tcp是面向字节流的
接口
listen(socket,gbacklog);
socket 设置成监听状态每次监听xxx
int accept(int socket, struct sockaddr restrict address,socklen_t restrict address_len);
参数1: socketfd网络文件描述符
参数2:指向
sockaddr
结构的指针 客户端地址信息 输出型参数参数3:指针指向sockaddr 的长度
accept 函数的返回值是一个文件描述符
accept 参数 和 recvfrom 参数
accept
用于服务器端的 TCP 套接字,用于接受客户端的连接请求。recvfrom
用于数据报套接字(如 UDP),用于接收数据并获取发送方的地址信息。accept
返回一个新的文件描述符,用于与客户端通信。recvfrom
返回接收到的字节数。accept
可选地返回连接客户端的地址信息。recvfrom
可选地返回数据发送方的地址信息。accept
用于面向连接的协议recvfrom
用于无连接的协议
recvfrom 直接接收 信息
accept 建立通讯文件描述符后再建立连接
Read 和 Write 函数
Read函数
ssize_t read(int fd, void *buf, size_t count);
从指定的问文件描述符中读取数据
fd 文件描述符号
buf 指向一个缓冲区
count 从文件描述符读取的字节数
成功返回读取的实际字节的数 如果读取的字节数为0 说明已经读取到了字节的末尾EOF
失败返回-1 并设置 errno指示错误类型
Wirte函数
ssize_t write(int fd, const void *buf, size_t count);
向指定的文件描述符写入数据
fd 文件描述符
buf 指向的缓冲区
count 写入的字节数
成功返回写入的字节数
失败返回-1 并设置 errno
connect()函数
int connect(int socket, const struct sockaddr *address,
socklen_t address_len);
用来建立TCP 连接
socket
网络地址
网络地址长度
sudo netstat -altp/-nltp 查看所有的TCP连接
多进程版本/和多线程版本 的TCP通讯的文件描述符问题
守护进程
chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️ sleep 100 & 23:53:50
chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️ jobs 23:53:54
Job Group CPU State Command
2 107979 0% running sleep 100 &
1 107877 0% running sleep 100 &
chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️
创建多个进程
🤓☝️ ps -axj|head -1 && ps -axj | grep sleep 23:57:05
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
23172 108786 108786 23172 pts/5 108864 S 1000 0:00 sleep 100
23172 108829 108829 23172 pts/5 108864 S 1000 0:00 sleep 100
23172 108838 108838 23172 pts/5 108864 S 1000 0:00 sleep 100
23172 108848 108848 23172 pts/5 108864 S 1000 0:00 sleep 100
23172 108865 108864 23172 pts/5 108864 S+ 1000 0:00 grep --color=auto sleep
两个 作业在同一个 会话下
使用fg 命令 将后台的作业提到前台
fg xx
作业是可以相互i前后台转化的
这样的任务会受到用户登录 注销的影响
相应的接口
pid_t setsid(void);
取消自己是组长
🤓☝️cat deamon.hpp 15:03:47
#pragma once
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
#define DEV "/dev/null"
void dameoSelf(const char * curPath = nullptr)
{
// 进程忽略异常的信号
signal(SIGPIPE,SIG_IGN);
// 让自己不是改组的组长
if(fork() > 0)exit(0);
// 守护进程是孤儿进程的一种
pid_t n = setsid();
// 守护进程脱离终端 关闭 重定向以前进程默认打开的文件
// /dev/null 向其中写入的文件全部会丢弃
int fd = open(DEV,O_RDWR);
if(fd > 0)
{
dup2(fd,1);
dup2(fd,0);
dup2(fd,2);
close(fd);
}else{
close(0);
close(1);
close(2);
}
// 进程执行路径发生更改
if(!curPath)chdir(curPath);
}⏎