Linux 网络编程——TCP

时间:2021-06-28 10:21:05
环境:Linux  C
 
一、协议介绍
     TCP是面向连接的协议,提供可靠的数据传输;TCP协议的可靠传输基于三次握手、四次挥手以及确认重传机制实现。下面来具体展示下TCP的三次握手、四次挥手状态 Linux 网络编程——TCPLinux 网络编程——TCP
    大家都知道已经建立连接的TCP遇到网络丢包会有确认重传机制。在三次握手期间,如果A收到B的SYN+ACK,但是B没有收到A返回的ACK,此时B超时后会重传SYN+ACK,如果超过特定次数依然没有收到A的ACK,那么B向A发送RST包,关闭连接,避免A维护一个异常的连接。四次挥手也是类似,都是利用超时重传机制。
 
二、TCP网络编程
     先展示一段代码
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
 
int main(int argc, char *argv[])
{
    int fd;
    struct sockaddr_in addr;
    struct timeval timeo = {3, 0};
    //lingger参数1表示启用linger,5最多等待5秒或者socket发送队列中的消息发送完毕
    //再执行close操作
    struct linger linger_value = {1, 5};
    socklen_t len = sizeof(timeo);
 
    //创建TCP连接,SOCK_CLOEXEC指明在执行fork等创建子进程函数时,子进程将继承的该fd描述符默认关闭
    fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (argc == 4)
        timeo.tv_sec = atoi(argv[3]);
 
    //设置接受socket数据的等待时间,对read,recv,recvfrom等函数生效
    setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo, len);
 
    //设置该参数是为了保证在关闭前尽可能的将数据都发送出去
    setsockopt(fd, SOL_SOCKET, SO_LINGER, &linger_value, (socklen_t)sizeof(linger_value));
 
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    addr.sin_port = htons(atoi(argv[2]));
 
    //connect受SO_SNDTIMEO设置影响,最多等待timeo时间
    if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        if (errno == EINPROGRESS) {
            fprintf(stderr, "timeout\n");
            return -1;
        }
        perror("connect");
        return -1;
    }
    printf("connected\n");
 
    char *msg = "hello buddy !";
    //这里指定MSG_NOSIGNAL表示当fd已经关闭时,不会触发SIGPIPE信号(该信号的默认操作是关闭程序
    send(fd, msg, strlen(msg), MSG_NOSIGNAL);
 
    char buffer[1024] = {'\0'};
    //这里的MSG_DONTWAIT就是要求recv函数非阻塞执行 ,同给socket设置SO_NONBLOCK效果一样
    recv(fd, buffer, sizeof(buffer) - 1, MSG_NOSIGNAL | MSG_DONTWAIT);
 
    //向对端发送FIN报文,关闭该fd上所有的读写socket连接
    shutdown(SHUT_RDWR, fd);
 
    //关闭文件描述符,如果执行了fork函数且没有设置SOCK_CLOEXEC,需要在子进程中也执行close关闭继承的文件描述符
    close(fd);
 
    return 0;
}

注:该代码是客户端的一部分代码,服务端与这个类似,区别就是服务端用bind、listen、accept函数替代了connect函数来实现对服务端口的监听、新连接的处理。

  1. 编写多进程程序时,如果创建文件描述符(打开文件、创建socket等)都需要设置SOCK_CLOEXEC标识,避免出现副作用。
  2. 给socket设置SO_LINGER选项、调用close之前执行shutdown命令一般用于服务端,这样可以最大限度地保证将数据发送到客户端并且可以有效防止,因为fork导致的子进程在处理socket时调用close关闭fd描述符,由于fd原有计数是2,所以fd并未实际关闭,也就不会触发向客户端发送FIN报文结束连接的动作。因而,服务端就可能出现大量的CLOSE_WAIT的连接。
  3. 如果系统收到FIN报文,会让recv、read等函数返回0表示连接已经关闭。此时send、recv等读写函数再对目标fd进行读写,根据TCP协议规定,会收到一个RST报文,如果再读写的话系统就会发送SIGPIPE信号给调用进程,而该信号默认动作是结束进程。因此,要么调用信号处理函数将SIGPIPE捕获处理,要么像代码中那样设置MSG_NOSIGNAL选项避免产生SIGPIPE信号。
  4. SO_SNDTIMEO除了对send、write等有效外,还对connect有效;SO_RCVTIMEO除了对recv、read等有效外还对accept有效。
  5. 如果系统中出现大量的close_wait状态的TCP连接,说明是程序在收到对端的FIN报文后没有关闭相关的socket(有可能是fork这类函数造成的),这需要检查结束连接处理部分的代码。
  6. 如果系统中出现大量的time_wait状态的TCP连接,这种连接是正常的。出现这种连接都是由于主动关闭socket造成的,解决方案就是改成客户端主动结束连接、或者降低time_wait的时间。