Linux应用开发之网络套接字编程
套接字(Socket)是计算机网络数据通信的基本概念和编程接口,允许不同主机上的进程(运行中的程序)通过网络进行数据交换。它为应用层软件提供了发送和接收数据的能力,使得开发者可以在不用深入了解底层网络细节的情况下进行网络编程,屏蔽了应用程序对底层协议的操作,使得应用程序使用网络进行数据传输变得更简便,使代码更容易维护。socket英文直译为“插座”,可以理解为应用层调用网络服务的接口。
套接字主要由以下三个属性组成:
网络地址:通常是IP地址,用于标识网络上的设备。
端口号:用于标识设备上的特定应用或进程。端口号是一个16位的数字,范围从0到65535。
协议:如TCP(传输控制协议)或UDP(用户数据报协议),定义了数据传输的规则和格式。
根据数据传输方式的不同,主要有两种类型的套接字:
流套接字(Stream Sockets):基于TCP协议,提供面向连接、可靠的数据传输服务。数据像流水一样连续传输,接收方按发送顺序接收数据,适用于需要准确无误传输数据的应用,如网页服务器。
数据报套接字(Datagram Sockets):基于UDP协议,提供无连接的数据传输服务。每个报文段独立传输,可能会丢失或无法保证顺序,适用于对传输速度要求高但可以容忍一定丢包率的应用,如在线视频会议。
套接字通过封装TCP/IP协议细节,提供了一组API,允许应用程序创建套接字、绑定地址和端口、监听连接、接受连接、发送和接收数据等。在网络通信中,通常一个套接字负责监听和接受外部连接(服务器套接字),另一个套接字负责发起连接(客户端套接字)。
套接字的引入极大地简化了网络编程的复杂度,使得开发者可以专注于应用逻辑的实现,而无需深入了解网络协议栈的内部工作原理。通过使用套接字,可以轻松实现不同计算机之间的数据交换,支持构建分布式系统和多种网络应用。
网络字节序和主机字节序
在网络编程中,特别是在跨平台和网络通信时,字节序(Byte Order)是非常重要的概念。字节序指的是多字节数据在内存中的存储顺序。主要有两种字节序:
大端字节序(Big-Endian):高位字节存储在内存的低地址处,低位字节存储在高地址处。这种字节序遵循自然数字的书写习惯,也被称为网络字节序(Network Byte Order)或网络标准字节序,因为它在网络通信中被广泛采用,如IP协议就要求使用大端字节序。
小端字节序(Little-Endian):低位字节存储在内存的低地址处,高位字节存储在高地址处。这是Intel x86-64架构以及其他一些现代处理器普遍采用的字节序,称为主机字节序(Host Byte Order)。
在网络通信中,为了让不同字节序的主机能够相互理解对方的数据,常常需要进行字节序转换。例如,发送数据前需要将主机字节序转换为网络字节序,接收数据后则需要将网络字节序转换为主机字节序。例如,如果你有一个IP地址或端口号(通常存储为整数)需要在网络上传输,就需要先使用htons()或htonl()将其转换为网络字节序,然后在网络另一端接收时,使用ntohs()或ntohl()将其转换回主机字节序。这样可以确保数据在网络中的传输不受不同主机字节序的影响。
htol函数
作用是将无符号整数 hostlong 从主机字节顺序(h)转换为网络字节顺序(n),一般用于ip地址的转换。
函数原型为uint32_t htonl(uint32_t hostlong);
htos函数
作用是将无符号短整数 hostshort 从主机字节顺序(h)转换为网络字节顺序(n)。一般用于端口号的转换。
函数原型为uint16_t htons(uint16_t hostshort);
ntohl函数
作用是将无符号整数 netlong 从网络字节顺序(n)转换为主机字节顺序(h)。
函数原型为uint32_t ntohl(uint32_t netlong);
ntohs函数
作用是将无符号短整数 netshort 从网络字节顺序(n)转换为主机字节顺序(h)。
函数原型为uint16_t ntohs(uint16_t netshort);
用于将ip地址转换为网络字节序的函数推荐以下几种
inet_aton函数
函数原型为int inet_aton(const char *cp, struct in_addr *inp);
作用是将来自 IPv4 点分十进制表示法的 Internet 主机地址 cp 转换为二进制形式(以网络字节顺序)并将其存储在 inp 指向的结构体中。
return int 成功返回 1;失败 返回 0
inet_pton函数
函数原型为int inet_pton(int af, const char *src, void *dst);
作用是字符串格式转换为sockaddr_in格式
int af: 通常为 AF_INET 用于IPv4地址,或 AF_INET6 用于IPv6地址
char *src: 包含IP地址字符串的字符数组,如果是IPv4地址,格式为点分十进制(如 "192.168.1.1");如果是IPv6地址,格式为冒号分隔的十六进制表示(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
void *dst:指向一个足够大的缓冲区(对于IPv4是一个struct in_addr结构体,对于IPv6是一个struct in6_addr结构体),用于存储转换后的二进制IP地址
return int : 成功转换返回0; 输入地址错误返回1;发生错误返回-1
inet_ntoa函数
函数原型为char *inet_ntoa(struct in_addr in);
作用是将主机字节序存储的ip地址转换为字符型
in 将以网络字节顺序给出的 Internet 主机地址 in 转换为 IPv4 点分十进制表示法的字符串。字符串存储在静态分配的缓冲区中,后续调用将覆盖该缓冲区。
return char* 缓冲区指针
TCP协议开发常用函数
socket函数
函数原型为int socket(int domain, int type, int protocol);
作用是在通信域中创建一个未绑定的socket,并返回一个文件描述符,该描述符可以在后续对socket进行操作的函数调用中使用
domain用于指定要创建套接字的通信域。一般使用以下三种。
AF_UNIX:本地通信,通常用于 UNIX 系统间的进程间通信。
AF_INET:IPv4 互联网协议。
AF_INET6:IPv6 互联网协议。
type 用于指定要创建的socket类型,一般使用以下两种
SOCK_STREAM:提供序列化、可靠的、双向的、基于连接的字节流。可以支持带外数据传输机制。
SOCK_DGRAM:支持数据报(无连接、不可靠的固定最大长度的消息)。
使用TCP协议就选第一个,UDP协议选第二个
这个参数还可以和以下两种配合使用,可以同时设置
SOCK_NONBLOCK:在新文件描述符引用的打开文件描述符上设置 O_NONBLOCK 文件状态标志(参见 open(2))。使用此标志可以节省调用 fcntl(2) 来实现相同结果的额外调用。
SOCK_CLOEXEC:在新文件描述符上设置关闭时执行(FD_CLOEXEC)标志。
protocol 指定要与socket一起使用的特定协议。指定协议为 0 会导致 socket() 使用适用于所请求的socket类型的未指定的默认协议,一般设置为0即可,
成功创建则返回文件描述符,失败返回-1
bind函数
函数原型为int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用是当使用 socket创建套接字时,它存在于一个名称空间(地址族)中,但没有为其分配地址。bind() 将由 addr 指定的地址(ip地址和端口号)分配给文件描述符 sockfd 所引用的套接字。addrlen 指定了 addr 指向的地址结构的大小(以字节为单位)。传统上,这个操作被称为“给套接字分配一个名称”
ockfd 套接字文件描述符
addr 指定的地址。地址的长度和格式取决于socket的地址族
addrlen addr 指向的地址结构的大小(以字节为单位)。填sizeof获取struct sockaddr类型变量的大小
return int 成功 0 失败 -1
地址族的概念
在网络编程中,地址族(Address Family)指定了套接字(socket)使用的网络协议类型以及地址的格式。简而言之,地址族决定了网络通信的范围和方式,比如是在同一台机器上的进程间通信,还是在网络上不同主机间的通信。每种地址族都支持特定类型的通信协议和地址格式。下面是一些常见的地址族:
① AF_INET
代表IPv4网络协议的地址族,使用32位地址。
主要用于互联网上的通信。
地址格式通常为点分十进制,如192.168.1.1。
② AF_INET6
代表IPv6网络协议的地址族,使用128位地址。
是IPv4的后继,旨在解决IPv4地址耗尽问题,并提供更多的功能。
地址格式为冒号分隔的十六进制,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。
③ AF_UNIX (或 AF_LOCAL)
用于同一台机器上的进程间通信(IPC)。
使用文件系统路径名作为地址。
这种方式不通过网络层进行数据传输,而是在操作系统内部完成,因此效率较高。
上述函数都使用了struct sockaddr结构体,他存储了地址族和ip地址,端口等信息,声明如下
struct sockaddr {
sa_family_t sa_family; // 地址家族,如 AF_INET、AF_INET6、AF_UNIX 等
char sa_data[14]; // 用于存储具体地址数据的数组,其布局取决于地址
}
如果我们使用常见的IPV4,IPV6,这样将ip地址,端口号一起放在一个char数组中,很难对其进行读取或者赋值,所以对于IPV4协议,在设置时使用struct sockaddr_in结构体结合pton函数直接转换为网络字节序更加方便,在调用函数时直接强转为struct sockaddr型即可
struct sockaddr_in {
sa_family_t sin_family; /* 地址族:AF_INET */
in_port_t sin_port; /* 端口号,网络字节顺序 */
struct in_addr sin_addr; /* 互联网地址 */
};
sin_family 总是设置为 AF_INET。
sin_port 包含端口号,以网络字节顺序表示。低于 1024 的端口号称为特权端口(或有时称为:保留端口)。只有特权进程(在 Linux 中:具有 CAP_NET_BIND_SERVICE 用户命名空间中的权限,控制其网络命名空间)可以绑定到这些套接字。
例如
struct sockaddr_in server_addr
inet_pton(AF_INET,"0.0.0.0",&server_addr.sin_addr); //本机ip地址可以表示为0.0.0.0
server_addr.sin_port = htons(6666);
listen函数
函数原型为int listen(int sockfd, int backlog);
作用是将 sockfd 指定的套接字标记为被动套接字,即将用于使用 accept接受传入的连接请求。由服务端调用
sockfd 监听连接请求的套接字文件描述符,也就是客户端的套接字文件描述符,要求该描述符已经通过bind函数绑定到一个本地地址。
backlog 未被及时响应的连接可以被放入队列等待连接,该参数指定等待队列可以容纳的最大连接数
return int 成功 0 失败 -1
accept函数
函数原型为int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用是从监听套接字 sockfd 的待处理连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回指向该套接字的新文件描述符。返回的是客户端套接字的文件描述符,(是返回的可以和客户端通信的文件描述符,也可以理解为是客户端的文件描述符,可以通过此来和客户端进行通信)新创建的套接字不处于监听状态,就与之后主动连接的客户端的套接字形成了一对一的关系,原始套接字 sockfd 不受此调用的影响。如果调用之后,没有客户端来连接,就会挂起等待(阻塞),等到接收到为止。
sockfd 一个使用 socket(2) 创建、使用 bind(2) 绑定到本地地址,并在 listen(2) 后监听连接的套接字。
addr 要么是一个空指针,要么是一个指向 sockaddr 结构的指针,用于返回连接socket的地址,即客户端的ip地址和端口号信息
addrlen 如果 address 不是空指针,则为一个指向 socklen_t 对象的指针,该对象在调用前指定提供的 sockaddr 结构的长度,并在调用后指定存储地址的长度。先定义一个socklen_t 类型的变量,用sizeof接收大小,然后传入指针
return int 返回一个新的套接字文件描述符,用于与客户端通信,所以可以看做是客户端的文件描述符,如果失败返回-1,并设置errno来表示错误原因
connect函数
函数原型为int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
由客户端调用,作用是与服务端建立连接。
sockfd 客户端套接字的文件描述符
addr 指向sockaddr结构体的指针,包含目的地地址信息
addrlen 指定addr指向的结构体的大小
return int 成功 0,失败 -1,并设置errno以指示错误原因
send函数
函数原型为ssize_t send(int sockfd, const void *buf, size_t len, int flags);
用于向另一个套接字传输消息。默认会阻塞,需要不阻塞的需要设置flags
sockfd 发送套接字的文件描述符。
buf 发送缓冲区,并非操作系统分配为服务端和客户端分配的缓冲区,而是用户为了发送数据,自己维护的字节序列。const修饰表名它是“只读”的,即send函数不会修改这块内存的内容。
len 要发送的数据的字节长度。它决定了从buf指向的缓冲区中将发送多少数据。
flags flags 对于大多数应用,这个参数被设置为0,表示不使用任何特殊行为。
MSG_DONTWAIT 启用非阻塞操作;如果操作会阻塞,则返回 EAGAIN 或 EWOULDBLOCK。这提供了类似于设置 O_NONBLOCK 标志(通过 fcntl(2) F_SETFL 操作)的行为,但不同之处在于 MSG_DONTWAIT 是一个每次调用的选项,而 O_NONBLOCK 是对打开文件描述符(参见 open(2))的设置,将影响调用进程中的所有线程以及持有引用相同打开文件描述符的其他进程。
return ssize_t成功发送的字节数。如果出现错误,它将返回-1,并设置errno以指示错误的具体原因。
recv函数
函数原型为ssize_t recv(int sockfd, void *buf, size_t len, int flags);
作用是从套接字关联的连接中接收数据。默认会阻塞,设置不阻塞需要设置flags
sockfd 套接字文件描述符。
buf 接收缓冲区,同样地,此处也并非内核维护的缓冲区。
len 缓冲区长度,即buf可以接收的最大字节数。
flags flags 参数是以下标志之一或多个的按位或。对于大多数应用,这个参数被设置为0,表示不使用任何特殊行为。
MSG_DONTWAIT 启用非阻塞操作;如果操作会阻塞,则调用失败
MSG_ERRQUEUE 此标志指定应该从套接字错误队列中接收排队的错误。
MSG_OOB 此标志请求接收在正常数据流中不会接收到的带外数据。
MSG_PEEK 此标志导致接收操作从接收队列的开头返回数据,而不从队列中删除该数据。因此,后续的接收调用将返回相同的数据。
MSG_TRUNC 对于原始(AF_PACKET)、Internet 数据报、netlink和 UNIX 数据报套接字:返回报文段或数据报的实际长度,即使它比传递的缓冲区长。
MSG_WAITALL 此标志请求操作阻塞,直到满足完整的请求。
return ssize_t 返回接收到的字节数,如果连接已经正常关闭,返回值将是0。如果出现错误,返回-1,并且errno变量将被设置为指示错误的具体原因。
shutdown函数
函数原型为int shutdown(int sockfd, int how);
作用是关闭套接字的一部分或全部连接
sockfd 套接字文件描述符
how 指定关闭的类型。其取值如下:
SHUT_RD:关闭读。之后,该套接字不再接收数据。任何当前阻塞在recv调用上的操作都将返回0。只会影响本端,不会触发挥手。从数据角度来看,套接字上接收缓冲区已有的数据将被丢弃,如果再有新的数据流到达,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。此操作只影响本端,不会触发挥手操作
SHUT_WR:关闭写。之后,试图通过该套接字发送数据将导致错误。如果使用此选项,TCP连接将发送一个FIN包给连接的对端,表明此方向上的数据传输已经完成。此时对端的recv调用将接收到0。
关闭连接的“写”这个方向,这就是常被称为”半关闭“的连接。此时,不管套接字引用计数的值是多少,都会直接关闭连接的写方向。套接字上发送缓冲区已有的数据将被立即发送出去,并发送一个 FIN 报文给对端。应用程序如果对该套接字进行写操作会报错。
SHUT_RDWR:关闭读写。同时关闭套接字的读取和写入部分,等同于分别调用SHUT_RD和SHUT_WR。之后,该套接字既不能接收数据也不能发送数据。相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。
return int 成功 0 失败 -1,并设置errno变量以指示具体的错误原因。
close函数
函数原型为int close(int __fd);
用于关闭一个之前通过open()、socket()等函数打开的文件描述符
close会对套接字引用计数减一,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流。但是close调用之后,引用计数并不一定会降到0,如果没有降到0就不会触发四次挥手。但是在当前进程内一定无法使用该套接字进行读/写。
在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。
如果对端没有检测到套接字已关闭,还继续发送报文,就会收到一个 RST 报文,告诉对端:“Hi, 我已经关闭了,别再给我发数据了。
int __fd: 这是一个整数值,表示要关闭的文件描述符
return: 成功关闭文件描述符时,close()函数返回0,失败返回-1,例如试图关闭一个已经关闭的文件描述符或系统资源不足,close()会返回-1
shutdown和close函数的区别
TCP通信中,套接字也是通过文件描述符操控的,底层同样存储在struct file结构体中,socket相关的数据存在该类型结构体实例的私有数据字段,因此,我们通过close()关闭套接字,效果和关闭文件是类似的,都是使得底层文件描述的引用计数减一,若引用计数减为0则释放套接字相关的资源。
1、close 会关闭当前进程下的连接,并释放所有连接对应的资源,而 shutdown 并不会释放掉套接字和所有的资源。
2、close 存在引用计数的概念,并不一定导致该套接字不可用;shutdown 则不管引用计数,直接使得该套接字不可用,如果有别的进程企图使用该套接字,将会受到影响。
3、close 的引用计数导致不一定会发出 FIN 结束报文,而 shutdown 则总是会发出 FIN 结束报文,这在我们打算关闭连接通知对端的时候,是非常重要的
4、close 函数并不能帮助我们关闭连接的一个方向, shutdown 函数才可以。
UDP协议开发常用函数
UDP通讯也使用socket,与TCP的主要差别在于使用流程,接收和发送的函数与TCP不一样。由于UDP是无连接的传输方式,不存在握手这一步骤,所以在绑定地址之后,服务端不需要listen,客户端也不需要connect,服务端同样不需要accept,只要服务端绑定以后,就可以相互发消息了,由于没有握手过程,两端都不能确定对方是否收到消息,这也是UDP协议不如TCP协议可靠的地方。
同样的在使用UDP协议的过程时,如果使用IPV4,那么也可以使用struct sockaddr_in结构体来方便我们设置IP地址和端口号,在调用相关函数时再强转为struct sockaddr型。
recvfrom函数
函数原型为ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
作用是将接收到的消息放入缓冲区 buf 中。默认是阻塞的
sockfd 套接字文件描述符 自己的套接字
buf 缓冲区指针
len 缓冲区大小
flags 通信标签,详见recv方法说明
src_addr 可以填NULL,如果 src_addr 不是 NULL,并且底层协议提供了消息的源地址,则该源地址将被放置在 src_addr 指向的缓冲区中。从而获取数据来源的地址信息,用于之后的发送
addrlen 如果src_addr不为NULL,它应初始化为与 src_addr 关联的缓冲区的大小。返回时,addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小,则返回的地址将被截断;在这种情况下,addrlen 将返回一个大于调用时提供的值。
return ssize_t 实际收到消息的大小。如果接收失败,返回-1
sendto函数
函数原型为ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
作用是向指定地址发送缓冲区中的数据(一般用于UDP模式)
sockfd 套接字文件描述符 也是自己的套接字
buf 缓冲区指针
len 缓冲区大小
flags 通信标签,详细见send方法说明,同样一般为0
dest_addr 目标地址。如果用于连接模式,该参数会被忽略
addrlen 目标地址长度
return ssize_t 发送的消息大小。发送失败返回-1