主机字节序和网络字节序
字节序分为大端字节序和小端字节序。现代PC大多采用小端字节序,因此小端字节序又称为主机字节序
。
当两台字节序不同的主机之间进行通信时,由于字节序的不同就会导致对数据的错误解释。但是接受端也不知道发送端发送过来的数据的字节序到底是大端还是小端,解决办法是:发送端总是把要发送的数据转化为大端字节序数据后再发送
,而接受端可以根据自身采用的字节序决定是否对接收到的数据进行转换。因此大端字节序也称为网络字节
,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
另外需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由Java编写)通信,也要考虑字节序的问题(Java虚拟机采用大端字节序)。
在linux中提供了下面4个函数来完成主机字节序和网络字节序之间的转换:
1 |
|
它们的含义很明确,比如htonl表示”host to network long”,即将32位无符号数从主机字节序转换为网络字节序数据。
socket
通用socket地址
Linux C Socket网络编程接口表示socket地址的结构体sockaddr,其定义如下:
1 |
|
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocal family,也称domain,见后文)和对应的地址族如下所示:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data成员用于存放socket地址值。但是,不同协议族的地址值具有不同的含义和长度,如下表:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可到108字节 |
PF_INET | 16bit端口号和32bit IPv4地址,共6字节 |
PF_INET6 | 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节 |
由上表可见,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:
1 |
|
这个结构体不仅能提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
专用socket地址
上面的两个通用socket地址结构体显然都不好用
,所以Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地协议族使用如下专用socket地址结构体:
1 |
|
TCP/IP协议族有socket_in和sockaddr_in6两个专业socket地址结构体,它们分别用于IPv4和IPv6:
1 | struct sockadddr_in{ |
所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket类型sockaddr(强制转换即可),因为socket编程接口使用的地址参数的类型都是sockaddr
。
IP地址转换函数
通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分10进展字符串表示IPv4地址,以及用16进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数才能使用。而记录日志时则相反,我们要把整数表示的IP地址转为可读的字符串。Linux提供了3个用于IP地址形式转化的函数:
1 |
|
Inet_addr函数将用点十进制字符串表示的IPv4地址转为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败时返回0
Inet_ntoa函数将用网络字节序表示的IPv4地址转化为用点十进制表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的,下面的代码揭示了其不可重入性:
1 | char *value1 = inet_ntoa(inet_addr("1.2.3.4")); |
下面这对更新的函数也能完成和前面3个函数相同的功能,并且它们同时适用于IPv4地址和IPv6地址。
1 |
|
inet_pton函数用于将字符串表示的IP地址src(用点十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转化成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。其中af参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败时返回0。
inet_ntop函数将网络字节序整数表示的IP地址,转化为IP字符串。前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定字符串dst目标存储单元的大小。有两个宏能帮助我们快速指定这个大小(分别用于IPv4和IPv6):
1 |
创建socket
UNIX/Linux的一个思想就是:所有的东西都是文件。socket也不例外,他就是可读、可写、可控制、可关闭的文件描述符。
下面的socket系统调用可创建一个socket:
1 |
|
- domin 参数告诉系统使用哪个底层协议族。对于TCP/IP协议族而言,该参数应该设置为PF_INET(Protocal Family of Internet,用于IPv4)或PF_INET6(用于IPv6),对于UNIX本地协议族而言,该参数应设置为PF_UNIX
- type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_DGRAM(数据报)服务。对于TCP/IP协议族而言,SOCK_STEAM表示传输层使用TCP协议,SOCK_DGRAM表示传输层使用UDP协议。
- protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一,几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。
socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。
绑定socket
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。需要将一个socket与socket地址进行绑定。通常在服务器端中,我们需要进行socket地址绑定,因为只有绑定后,客户端才能知道该如何连接它。客户端通常不需要绑定socket地址,而是采用匿名方式,即使用操作系统自动分配的socket地址。
1 |
|
bind将addr所指的socket地址分配给未绑定的sockfd文件描述符,addrlen参数指出该socket地址长度。
监听socket
socket被命名之后,还不能马上接收客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户端连接:
1 |
|
- sockfd参数指定被监听的sock文件描述符
- backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接。客户端也将受到ECONREFUSED错误。在内核版本2.2之前的linux中,backlog参数是值所有处于半连接状态和完全连接状态的socket上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket上限,处于半连接状态的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义
listen成功返回0,失败时返回-1并设置errno
接收连接
下面的系统调用从listen的监听队列中接收一个连接:
1 |
|
sockfd参数是执行过listen系统调用的监听socket文件描述符。addr参数用来获取客户端的socket地址,该socket地址的长度由addrlen参数返回。
accept成功时返回一个新的连接socket文件描述符
,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1,并设置errno。
当当前监听队列中没有连接时,accept会被阻塞。
accept只是从监听队列中取出连接,而不管取出连接后,连接处于何种状态(连接或者断开)
。
发起连接
在客户端需要通过一下系统调用来主动与服务器建立连接:
1 |
|
- sockfd是由socket系统调用返回的socket文件描述符
- serv_addr是服务器监听的socket地址
- addrlen参数则serv_addr的长度
connect成功时返回0,一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno,其中两种常见的errno是ECONNREFUSED和ETIMEOUT,它们的含义分别是连接被拒绝和连接超时。
关闭连接
关闭连接实际上就是关闭连接对应的socket文件描述符,可以通过调用关闭普通文件描述符的系统调用来完成:
1 |
|
不过,close系统调用并非总是立即关闭一个连接
,而是将fd的引用记数减一,只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将父进程中打开的socket文件描述符的引用计数加1
。如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用shutdown系统调用(相对于close来说,它是专门为网络编程设计的)。
1 |
|
howto参数决定了shutdown的行为:
可选值 | 含义 |
---|---|
SHUT_RD | 关闭sockfd上读的这一半,应用程序不能再针对socket文件描述符号执行读操作,并且该socket接收缓冲区中的数据都将被丢弃 |
SHUT_WR | 关闭sockfd上写的这一半。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socke文件描述符执行写操作。这种情况下,连接处于半关闭状态。 |
SHUT_RDWR | 同时关闭sockfd上的读和写 |
由此可见,shutdown能分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。shutdow成功时返回0,失败时返回-1,并设置errno。
socket服务端,客户端示例
服务端
1 |
|
客户端
1 |
|
数据读写
TCP数据读写
对文件的读写操作read和write同样适用于socket,但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
1 |
|
- recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次recv,才能读取到完整的数据。
recv可能返回0,这意味着通信对方已经关闭连接了
。recv出错时返回-1并设置errno。 - send往sockfd上写入数据,buf和len参数分别指定缓冲区的位置和大小。send成功时返回实际写入的数据长度,失败则返回-1并设置errno。
recv和send函数中的flag参数为数据收发提供了额外的控制,它可以取下表所示选项中的一个或几个的逻辑或
选项名 | 含义 | send | recv |
---|---|---|---|
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,直到得到答复,它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地知道目标主机就在本地网络上 | Y | N |
MSG_DONTWAIT | 对socket的此次操作时非阻塞的。socket的读写操作默认是阻塞的。 | Y | Y |
MSG_MORE | 告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可防止TCP发送过多小的报文段,从而提高效率 | Y | N |
MSG_WAITALL | 读操作仅在读取指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓存的数据,此次读操作不会导致这些数据被清除 | N | Y |
MSG_OOB | 发送或接收紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或者socket连接中写数据不引发SIGPIPE信号 | Y | N |
UDP数据读写
socket编程接口中用于UDP数据报读写的系统调用是:
1 |
|
recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数scr_addr所指的内容,addrlen参数则指定该地址的长度。
sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接受端的socket地址,addrlen参数则指定该地址的长度。
这两个系统调用的flags参数以及返回值的含义均于send/recv系统调用的flags参数及返回值相同。
另外,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket数据读写,只需要把最后两个参数都设置为NULL即可。
带外标记检查
当Linux内核检查到TCP紧急标志时,将通知应用程序有带外数据需要接受。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有外带数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据,可以通过以下函数来实现:
1 |
|
sockadmark判断sockfd是否处于带外标记,即下一个读取到的数据是否是带外数据,如果是,sockatmark返回1,此时就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
socket选项
Linux中有下面两个系统调用是专门用来读取和设置socket文件描述符属性的方法:
1 |
|
sockfd参数指定被操作的目标socket文件描述符。level参数指定要操作哪个协议的选项,比如说IPv4、IPv6、TCP等。option_name参数则指定选项的名字。
下表列举了socket通信中几个比较常用的socket选项。option_value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值。
level | option name | 数据类型 | 说明 |
---|---|---|---|
SOL_SOCKET(通用socket选项,与协议无关) | SO_DEBUG | int | 打开调试信息 |
SO_REUSEADDR | int | 重用本地地址 | |
SO_TYPE | int | 获取socket类型 | |
SO_ERROR | int | 获取并清除socket错误状态 | |
SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机。含义和send方法的MSG_DONTROUTE标志类型 | |
SO_RCVBUF | int | TCP接收缓冲区大小 | |
SO_SNDBUF | int | TCP发送缓冲区大小 | |
SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 | |
SO_OOBINLINE | int | 接收到的带外数据将保留在普通数据的输入队列中,此时我们不能使用带MSG_OOB标志的读操作来读取带外数据,而应该像读取普通数据那样读取带外数据 | |
SO_LINGER | linger结构体 | 若缓冲区中还有数据待发送,则延迟关闭 | |
SO_RCVLOWAT | int | TCP接收缓存区低水位标记 | |
SO_SNDLOWAT | int | TCP发送缓存区低水位标记 | |
SO_RCVTIMEO | timeval | 接收数据超时 | |
SO_SNDTIMEO | timeval | 发送数据超时 | |
IPPROTO_IP | IP_TOS | int | 服务类型 |
IP_TTL | int | 存活时间 | |
IPPROTO_IPV6 | IPV6_NEXTHOP | sockaddr_in6 | 下一跳IP地址 |
IPV6_RECVPKTINFO | int | 接收分组信息 | |
IPV6_DONTFRAG | int | 禁止分片 | |
IPV6_RECVTCLASS | int | 接收通信类型 | |
IPPROTO_TCP | TCP_MAXSEG | int | TCP最大报文段大小 |
TCP_NODELAY | int | 禁止Nagle算法 |
getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno
值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前对socket设置才有效
。这是因为连接socket只能有accept调用返回,而accept从监听队列中接受的连接至少已经完成TCP三次握手的前两个步骤
,这说明服务器已经向客户端发出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项。这种情况Linux给开发人员提供的解决方案是:在调用listen前,对socket设置的这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些选项选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SDNLOWAT、TCP_MAXSEG、TCP_NODELAY
。而对客户端而言,这些socket选项则应该在调用connect函数之前设置
,因为connect调用成功返回之后,TCP三次握手已完成。
SO_REUSEADDR选项
对于处于TIME_WAIT状态的TCP连接,服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT状态的连接占用的socket地址。
1 | int sockfd = socket(PF_INET, SOCK_STREAM, 0); |
经过setsockopt的设置后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。此外我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本不进入TIME_WAIT状态,进而允许应用程序立即重用本地的socket地址。
SO_RCVBUF和SO_SNDBUF选项
SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区的发送缓冲区的大小。不过,当我们用setsockopt来设置TCP的接受缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP接受缓冲区的最小值时256字节,而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认值)。系统这样做的目的,主要是确保一个TCP连接有足够的空闲缓冲区来处理拥塞(比如说快重传算法就期望TCP接收缓冲区能至少容纳4个大小为最大报文段长度的TCP报文段)。此外,我们可以直接修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。
SO_RCVLOWAT和SO_SNDLOWAT选项
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记,它们一般被I/O复用系统调用用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总量大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间
(可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写数据。
默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节
。
SO_LINGER选项
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭socket时,close将立即返回,TCP模块负责将socket对应的TCP发送缓冲区中残留的数据发送给对方。
设置(获取SO_LINGER选项值时),我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:
1 |
|
根据linger结构中两个成员变量的不同值,close系统调用可能产生如下3中行为之一:
- l_onoff等于0,此时SO_LINGER选项不起作用,close用默认行为来关闭socket
- l_onoff不为0,l_linger等于0。此时close系统调用立即返回,TCP模块将
丢弃
被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供一了异常终止一个连接的方法。 - l_onoff不为0,l_linger大于0。此时close的行为取决于两个条件:一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;二是该socket是阻塞的,还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送玩所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送玩残留数据并得到对方确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。