【项目 计网8】4.23 TCP状态转换 4.24半关闭、端口复用

文章目录


4.23 TCP状态转换


  • 2MSL(Maximum Segment Lifetime)
    主动断开连接的一方,最后进入一个TIME_WAIT状态,这个状态会持续:2msl
    msl:官方建议:2分钟,实际是30s

当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。

这样就能够==让 TCP 连接的主动关闭方在它变成TIME_WAIT状态以后发送的最后一个 ACK 丢失的情况下,重新发送最终的 ACK。==最后这个ACK如果没有被B接收到(超时重传的计时内,小于2MSL),那么B就会再发一个FIN给A。

主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。

关于三次握手四次挥手

在握手的时候,第二次握手如果服务器不同意建立连接,直接不发ACK就可了。如果同意就发送ACK。当中没有数据牵扯。

但是!断开连接的时候,比如A发完了所有消息准备断开。就会有前两次挥手。但此时B很有可能还有信息要发送给A。所以二三次挥手之间可能会间隔很长时间。直到B也决定断开连接,才会有三四次挥手。

注意:一旦一二次挥手成功,A就只能回复B的消息收到了,而不能再给B发送新的报文。因为此时A到B的连接以及断开。只是B到A还没断开。

B一般会受到所有ACK以后才会进行第三四次挥手。

4.24半关闭、端口复用

  • 半关闭

当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

注意:

  1. 如果有多个进程(多线程)共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
  2. 在多进程(多线程)中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。
  3. 因为 shutdown是用于改变底层套接字的状态的,而close改变的只是文件描述符的引用计数。

tcp_server.c

cpp 复制代码
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);
    
    //int optval = 1;
    //setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

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

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

    // 接收客户端连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
        perror("accpet");
        return -1;
    }

    // 获取客户端信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);

    // 输出客户端的信息
    printf("client's ip is %s, and port is %d\n", cliIp, cliPort );

    // 接收客户端发来的数据
    char recvBuf[1024] = {0};
    while(1) {
        int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
        if(len == -1) {
            perror("recv");
            return -1;
        } else if(len == 0) {
            printf("客户端已经断开连接...\n");
            break;
        } else if(len > 0) {
            printf("read buf = %s\n", recvBuf);
        }

        // 小写转大写
        for(int i = 0; i < len; ++i) {
            recvBuf[i] = toupper(recvBuf[i]);
        }

        printf("after buf = %s\n", recvBuf);

        // 大写字符串发给客户端
        ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
        if(ret == -1) {
            perror("send");
            return -1;
        }
    }
    
    close(cfd);
    close(lfd);

    return 0;
}

tcp_client.c

cpp 复制代码
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

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

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    while(1) {
        char sendBuf[1024] = {0};
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

端口复用

端口复用最常见的用途是:

  • 防止服务器重启时之前绑定的端口还未释放

  • 程序突然退出而系统没有释放端口

    (没有被释放的端口就无法再次建立连接)

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
//设置套接字属性(不仅仅能设置端口复用)set socket option
int setsockopt(int sockfd,int level,int optname,const void* optval,socklen_t optlen);
-sockfd:打开的套接字的文件描述符
-level:级别 SOL_SOCKET(端口复用的级别)
-optname:
	-SO_REUSEPORT	允许重用端口复用
	-SO_REUSEADDR   允许重用地址复用
-optval:端口复用的值(整形)
	-1:可以复用
	-0:不可以复用
-optlen:optval参数的大小

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

常看网络相关信息的命令:netstat (netstate)

参数:

  • a 所有的socket (all)

  • p 显示正在使用socket的程序名称(programs)

  • n 直接使用IP地址,而不通过域名服务器(numric)

cpp 复制代码
netstat -anp|grep 9999

有三个socket是因为服务器端有一个监听还有个数据通信的。

grep 9999:这个命令会筛选出包含 "9999" 这个数字的行。

所以整个命令的目的是查看哪些进程或连接正在使用端口 "9999"。

在通信过程中,状态不发生改变

相关推荐
Karoku06611 分钟前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
geek_Chen0113 分钟前
虚拟机共享文件夹开启后mnt/hgfs/下无sharefiles? --已解决
linux·运维·服务器
(⊙o⊙)~哦44 分钟前
linux 解压缩
linux·运维·服务器
衍生星球2 小时前
【网络安全】对称密码体制
网络·安全·网络安全·密码学·对称密码
掘根2 小时前
【网络】高级IO——poll版本TCP服务器
网络·数据库·sql·网络协议·tcp/ip·mysql·网络安全
友友马3 小时前
『 Linux 』HTTP(一)
linux·运维·服务器·网络·c++·tcp/ip·http
2401_872514973 小时前
深入探究HTTP网络协议栈:互联网通信的基石
网络·网络协议·http
重生之我在20年代敲代码3 小时前
HTML讲解(一)body部分
服务器·前端·html
清水白石0084 小时前
C++使用Socket编程实现一个简单的HTTP服务器
服务器·c++·http