[C++项目] Web Server(4):TCP 通信流程与端口复用

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

三次握手与四次挥手

建立连接


首先服务器通过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状态

四次挥手完成,连接断开成功

注意:

  1. 如果有多个进程共享一个套接字,close 每被调用1次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
  2. 在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响到其它进程。

数据传输

readwrite是linux系统对文件的读取操作,只不过在linux中万物皆文件,因此也可以用于socket通信

但是socket通信还有自己专门的通信函数:recvsend

接受数据

#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被设置为EAGAINEWOULDBLOCK

接收调用通常立刻返回任何可用的数据,直到达到请求的数量,而不是一直等待收到所请求的全部数据再一次性返回。

recv 参数说明:

  • sockfd:表示已连接的套接字的文件描述符。
  • buf:指向接收数据的缓冲区的指针。
  • len:表示接收缓冲区的大小。
  • flags:用于指定接收操作的可选标志,如 MSG_DONTWAITMSG_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_addraddrlen将被忽略(当它们不是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绑定端口之前

参数:

  • sockfdsockfd必须指向一个打开的套接字描述符

  • levellevel指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP)。在设置端口复用时,设置为SOL_SOCKET(端口复用的级别)。

  • optname:用于指定选项的名称,与level一块使用。在设置端口复用时一般用以下两个:

    • SO_REUSEADDR:允许重用本地地址
    • SO_REUSEPORT:允许重用本地端口
  • optvaloptval是一个指向某个变量的指针,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.