Linux网络编程(三)

1.TCP三次握手

TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。

TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。

TCP 提供了一种可靠、面向连接、字节流、传输层的服务,
用采 三次握手 建立一个连接
采用 四次挥手 关闭一个连接

三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
在这里插入图片描述

  • 16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。
  • 32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从B 到 A)的 TCP 报文段的序号值也具有相同的含义。
  • 32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。
  • 4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示15,所以 TCP 头部最长是60 字节。
  • 6 位标志位包含如下几项:
    – URG 标志,表示紧急指针(urgent pointer)是否有效。
    – ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
    – PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
    – RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
    – SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
    – FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
    – 16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收
    – 通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
    – 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。
    – 16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

三次握手:
在这里插入图片描述

第一次握手:
1.客户端将SYN标志置为1
2.生成一个随机的32位序号:seq = J

第二次握手:
1.服务器端接受客户端的连接:ACK = 1
2.服务器会回发一个确认序号:ack = 客户端 + 数据长度 + SYN/FIN(按一个字节算)
3.服务器端向客户端发起连接请求:SYN = 1
4.服务器会生成一个随机序号:seq = K

第三次握手:
1.客户端应答服务器连接请求:ACK = 1
2.客户端回复收到了服务器端的数据:ack = 服务器端的序号 + 数据长度 + SYN/FIN(按一个字节算)

2.TCP四次挥手

四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。

客户端和服务器端都可以主动发起断开连接,谁先调用close(),谁就发起。

在TCP连接的时候,采用三次挥手建立的连接时双向的,在断开的时候需要双向断开。
在这里插入图片描述

3.TCP滑动窗口

滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)

TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。

滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

窗口理解为缓冲区的大小,滑动窗口的大小会随着发送数据和接收数据而变化。通信的双方都有发送数据和接受数据的缓冲区。

服务器:
发送缓冲区(发送滑动窗口)
接收缓冲区(接收滑动窗口)
客户端:
发送缓冲区(发送滑动窗口)
接收缓冲区(接收滑动窗口)

在这里插入图片描述

发送方的缓冲区:
白色格子:空闲的空间
灰色格子:数据已经被发送出去,但还没有被接收
紫色格子:还没有发送出去的数据

接收方的缓冲区:
白色格子:空闲的空间
紫色格子:已经接收到的数据

在这里插入图片描述

mss : 一条数据的最大的数据量
win : 滑动窗口
1. 第一次握手,客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是4096
2. 第二次握手,服务器接收连接情况,告诉客户端服务器的滑动窗口是6144,一次发送的最大数据量是1024
3. 第三次握手
4.4-9次,客户端连续给服务器发送了6k的数据,每次发送1k
5.10次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2k,滑动窗口大小为2k
6.11次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4k,滑动窗口大小为4k
7.12次,客户端给服务器发送了1k的数据
8.13次,第一次挥手,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9.14次,第二次挥手,服务器回复ACK 8194:同意断开连接的请求,告诉客户端已经接收到刚才发送的2k数据,滑动窗口大小为2k
10.15-16次,通知客户端滑动窗口大小
11.17次,第三次挥手,服务器端给客户端发送给FIN,请求断开连接
12.18次,第四次挥手,客户端统一服务器端的断开请求

4.TCP通信并发

要实现TCP通信服务器并发的任务,使用多线程或者多进程来解决。

多进程时间并发服务器

思路:
     1.一个父进程,多个子进程
     2.父进程负责等待并接收客户端连接
     3.子进程:完成通信,接收一个客户端连接,就创建一个子进程用于通信

服务器端:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    // 创建socket地址并对成员初始化
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;
    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    // 监听
    ret = listen(lfd, 128);
    // 不断循环等待客户端连接
    while(1) {

        struct sockaddr_in cliaddr;    // 连接的客户端信息
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
            if(errno == EINTR) {
                continue;
            }
            perror("accept");
            exit(-1);
        }
        // 每连接一个客户端,就创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }

    }
    close(lfd);
    return 0;
}

客户端:

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}

多线程实现并发服务器

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
            perror("read");
            exit(-1);
        }else if(len > 0) {
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);

        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

5.TCP状态转换

在这里插入图片描述
状态转换发生在三次握手和四次挥手过程中,数据传输过程中状态不会改变。

三次握手:
第一次握手,客户端调用connect将SYN置为1发送报文头,此时客户端的状态从close()转换为SYN_SENT
第二次握手,服务器端一直调用listen(),状态为listen()状态,当接收到客户端发送的SYN,此时服务器端的状态从listen()状态转换为SYN_RCVD,并给回给客户端SYN、ACK;
第三次握手,当客户端接收到服务器端发送的SYN、ACK,此时客户端的状态转换为ESTABLISHED,并给回给服务器端ACK,此时服务器端的状态转换为ESTABLISHED

四次挥手:
假设由客户端发起第一次挥手:
第一次挥手,客户端向服务器端发送FIN请求,此时客户端的状态转换为FIN_WAIT_1;
第二次挥手,服务器端接收到FIN请求,此时服务器端的状态转换为CLOSE_WAIT,并回给客户端ACK,客户端接收到ACK,此时客户端的状态转换为FIN_WAIT_2;
第三次挥手,服务器端发送FIN请求,此时服务器端的状态转换为LAST_ACK;
第四次挥手,客户端接收到FIN请求,此时客户端的状态转换为TIME_WAIT,并回给服务器端ACK。
在这里插入图片描述
黑线为特殊情况下的状态转换,红线为客户端状态转换,绿线为服务器端状态转换。

TIME_WAIT定时经过两倍报文寿命后才会结束:

  • 2MSL(Maximum Segment Lifetime)
    主动断开连接的一方, 最后进出入一个 TIME_WAIT状态, 这个状态会持续: 2msl
    msl: 官方建议: 2分钟, 实际是30s
    当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。
    这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK
    主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。
  • 半关闭
    当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 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 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

注意:

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

6.端口复用

端口复用最常用的用途是:
1.防止服务器重启时之前绑定的端口还未释放
2.程序突然退出而系统没有释放端口

#include <sys/types.h> 
#include <sys/socket.h> 
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    参数:
         - sickfd:要操作的文件描述符
         - leevel:级别 -SOL_SOCKET(端口复用的级别)
         - optname:选项的名称
                   -SO_REUSEADDR
                   -SO_REUSEPORT
         -optval:端口复用的值(整型)
                 -1:可以复用
                 -0:不可以复用
         -optlen:optval参数的大小

注:端口复用,设置的时机是在服务器绑定之前。

查看网络相关信息的命令:

netstat
参数:  -a 所有socket
	   -p 显示正在使用socket的程序的名称
	   -n 直接使用IP地址,而不通过域名服务器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值