如何理解之前博客中的服务器和客户端的通信流程,这里以具体函数来解释整个通信过程中的状态转换
三次握手与四次挥手

建立连接
首先服务器通过listen
函数进入LISTEN
状态
接着客户端通过connect
函数发起连接,客户端进入SYN_SENT
状态
服务器收到连接请求后,回复一个ACK
进入SYN_RCVD
状态
客户收到服务器回复的ACK
后,对该ACK
回复一个ACK
,进入ESTABLISHED
状态
服务器收到客户端回的ACK
后也进入ESTABLISHED
状态
三次握手完成,连接建立成功
数据传输
连接建立完成后就可以进行客户端与服务器端的通信
在之前的TCP通信实例中,就是客户端通过write
发送数据,服务器端通过read
读取数据
断开连接
首先客户端或服务器端任意一方都可以主动使用close
函数发起断开连接的请求,进入FIN_WAIT_1
状态
另一方收到请求后回复ACK
并进入CLOSE_WAIT
状态
主动断链方收到ACK
后进入FIN_WAIT_2
状态,等待被动方处理完成发送断链请求
此时主动断链方进入半关闭状态直至收到被动方的
FIN
请求,此时主动断链方仍旧可以接受数据,但是不能发送数据
从程序的角度,可以使用API来控制实现半关闭/半连接状态
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd
:需要关闭的socket的描述符
how
: 允许为shutdown
操作选择以下几种方式:
SHUT_RD
(0):关闭sockfd
上的读功能,此选项将不允许sockfd
进行读操作。该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。SHUT_WR
(1): 关闭sockfd
的写功能,此选项将不允许sockfd
进行写操作。进程不能在对此套接字发出写操作。SHUT_RDWR
(2):关闭sockfd的读写功能。相当于调用shutdown
两次:首先是以SHUT_RD
,然后以SHUT_WR
。
使用close
中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。shutdown
不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
被动方处理完断链前的操作后,主动通过close
函数发送断链请求,进入LASK_ACK
状态
主动断链方收到请求后,回复ACK
,进入TIME_WAIT
状态,在等待一个固定的时间(两倍报文段寿命,2MSL
)后自动进入CLOSE
状态
这里等待的原因是为了保证最后这个
ACK
丢失后,可以收到被动方的重发请求,重发ACK
被动断链方收到ACK
后,进入CLOSE
状态
四次挥手完成,连接断开成功
注意:
- 如果有多个进程共享一个套接字,
close
每被调用1次,计数减1,直到计数为0时,也就是所用进程都调用了close
,套接字将被释放。 - 在多进程中如果一个进程调用了
shutdown(sfd, SHUT_RDWR)
后,其它的进程将无法进行通信。但如果一个进程close(sfd)
将不会影响到其它进程。
数据传输
read
和write
是linux系统对文件的读取操作,只不过在linux中万物皆文件,因此也可以用于socket通信
但是socket通信还有自己专门的通信函数:recv
,send
接受数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recv()
、recvfrom()
和recvmsg()
调用用于从套接字接收消息。 它们可用于接收无连接套接字和面向连接套接字上的数据。
这三个调用在成功完成后都会返回报文的长度。如果消息太长,无法在提供的缓冲区中容纳,多余的字节可能会被丢弃,这取决于接收消息的套接字类型。
recv()
函数只能用于从已连接的套接字(socket)接收数据。
如果在套接字上没有可用的消息,接收调用将等待消息的到来,除非套接字是非阻塞的,在这种情况下返回值为-1,并且外部变量errno
被设置为EAGAIN
或EWOULDBLOCK
。
接收调用通常立刻返回任何可用的数据,直到达到请求的数量,而不是一直等待收到所请求的全部数据再一次性返回。
recv
参数说明:
sockfd
:表示已连接的套接字的文件描述符。buf
:指向接收数据的缓冲区的指针。len
:表示接收缓冲区的大小。flags
:用于指定接收操作的可选标志,如MSG_DONTWAIT
、MSG_WAITALL
等。
需要注意的是,recv()
函数是一个阻塞函数,如果没有数据可接收,它会一直等待,直到有数据到达或发生错误。如果你希望在接收数据时设置超时或非阻塞模式,可以使用 select()
或 fcntl()
函数来实现。
发送数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
系统调用send()
、sendto()
和sendmsg()
用于向另一个套接字发送消息。
send()
调用只有在套接字处于连接状态时才能使用(这样才能知道目标接收者)。 send()
和write()
的唯一区别是是否有flags
参数。 在flags
参数为零的情况下,send()
等同于write()
。 另外,下面的调用
send(sockfd, buf, len, flags)
等价于
sendto(sockfd, buf, len, flags, NULL, 0);
参数sockfd
是发送套接字的文件描述符。
如果sendto()
在连接模式(SOCK_STREAM
, SOCK_SEQPACKET
)的套接字上使用,参数dest_addr
和addrlen
将被忽略(当它们不是NULL
和0时,可能返回错误EISCONN
)、 当socket没有被连接时,将返回错误ENOTCONN
。
否则,目标地址由dest_addr
给出,addrlen
指定其大小。
对于sendmsg()
,目标地址由msg.msg_name
给出,msg.msg_namelen
指定其大小。
send
参数说明:
sockfd
:表示已连接的套接字的文件描述符。buf
:指向要发送数据的缓冲区的指针。len
:表示要发送的数据的大小。flags
:用于指定发送操作的可选标志,如 MSG_DONTWAIT、MSG_NOSIGNAL 等。
send()
函数的返回值是实际发送的字节数,如果返回值为 -1,则表示发生了错误,可以通过 errno
变量获取具体的错误信息。
需要注意的是,send()
函数可能不会立即发送所有的数据,特别是在非阻塞模式下。如果你需要确保所有数据都被发送,可以使用循环来重复调用 send()
直到发送完所有数据。
查看网络信息
netstat
命令是一个用于显示网络状态和统计信息的命令行工具。它可以显示当前打开的网络连接、监听端口、路由表、网络接口等信息,是网络故障排除和性能调优的常用工具之一。
常用的参数:
- -a:显示所有连接和监听端口。
- -n:以数字形式显示地址和端口号,而不是使用主机名和服务名。
- -p:显示与连接关联的进程信息。
- -r:显示路由表。
- -i:显示网络接口信息。
- -s:显示网络统计信息。
下面是一些额外的示例:
显示所有 TCP 连接:
netstat -at
显示所有 UDP 连接:
netstat -au
显示所有监听端口:
netstat -l
显示所有与进程关联的连接:
netstat -p
查找所有使用端口号为 9999 的网络连接,并显示与之关联的进程信息:
netstat -anp| grep 9999
如果有多个进程使用了该端口号,则会显示多行输出。
端口复用
端口复用最常用的用途是:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
注意 端口复用的设置是在服务器调用bind
绑定端口之前
参数:
-
sockfd
:sockfd
必须指向一个打开的套接字描述符 -
level
:level
指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP)。在设置端口复用时,设置为SOL_SOCKET
(端口复用的级别)。 -
optname
:用于指定选项的名称,与level
一块使用。在设置端口复用时一般用以下两个:SO_REUSEADDR
:允许重用本地地址SO_REUSEPORT
:允许重用本地端口
-
optval
:optval
是一个指向某个变量的指针,setsockopt
从*optval
中取得选项待设置的新值,getsockopt
则把已获取的选项当前值存放到*optval
中。*optval
的大小由最后一个参数指定,它对于setsockopt
是一个值参数,对于getsockopt
是一个值结果参数。- 在设置端口复用时传进去的是一个int类型的指针,其指向的变量值为1则可以复用,为0则表示不可复用
-
optlen
:指定optval
的大小
返回值:
- 成功:0
- 失败:-1
使用示例
int opval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opval, sizeof(opval));
下图汇总了可由getsockcpt
获取或由setsockopt
设置的选项。其中的“数据类型”列给出了指针oprval必须指向的每个选项的数据类型。我们用后跟一对花括号的记法来表示一个结构,如1inger{}
就表示struct linger
.