牛客DAY3 多进程实现并发服务器

TCP通信并发

要实现tcp通信服务器处理并发的任务,使用多线程或者多进程来解决
思路:

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

TCP 状态转化

在这里插入图片描述
在TCP协议中,四次挥手是用于关闭连接的过程。在这个过程中,当客户端收到服务端的最后一个ACK确认报文后,它会进入TIME_WAIT状态,并且要等待2MSL(Maximum Segment Lifetime,最大报文生存时间)的时间。

等待2MSL的原因主要有两个:

  • 确保连接彻底关闭:客户端发送最后一个ACK报文给服务端后,服务端如果没有收到这个报文,会超时重传FIN报文,因此客户端必须保持等待状态以便收到这个重传FIN报文并给予响应。此外,如果客户端发送的ACK报文在网络中丢失了,那么服务端也会超时重传FIN报文,所以客户端必须等待足够长的时间以确保收到这个重传FIN报文并做出响应。等待2MSL的时间可以确保所有可能丢失的报文都已经被丢弃并让所有的报文在网络中消失。

  • 避免新连接的混淆:如果客户端在关闭连接后立即重新打开一个新的连接并使用之前的端口号,那么之前的那些丢失的报文可能会到达新连接中,从而产生混淆和错误。等待2MSL的时间可以确保所有与之前连接相关的报文都已经在网络中消失,不会影响新连接的正常通信。

因此,等待2MSL是TCP协议中重要的一个机制,能够确保连接的正确关闭并避免混淆和错误的发生。

半关闭

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

TCP协议中的半关闭(Half-close)状态是指在一个TCP连接中,其中一方先关闭了它的发送流(发送FIN报文),但是仍可以接收来自另一方的数据。

  • 在半关闭状态中,一方继续接收另一方发送的数据,但不再发送数据。这种状态可以让一个TCP连接中的一方先关闭它的发送流,以便让另一方能够继续发送数据,从而实现数据的双向传输。

  • 半关闭状态通常用于一个TCP连接中的一方完成了它的任务并希望关闭发送流,但仍需要接收来自另一方的数据。例如,当一个HTTP服务器向客户端发送请求的响应时,服务器在发送完响应数据后可以关闭发送流,但仍需要接收来自客户端的可能的ACK确认报文。在这种情况下,服务器可以进入半关闭状态以保持TCP连接的开启状态,从而可以接收来自客户端的数据。

需要注意的是,半关闭状态只是TCP连接的一种状态,它并不是TCP连接的终止状态。在半关闭状态下,TCP连接的另一方仍可以继续发送数据,直到该方也关闭它的发送流并向另一方发送FIN报文以终止连接。

       #include <sys/socket.h>

       int shutdown(int sockfd, int how);
       sockfd:需要关闭的socket的描述符
       how:允许为shutdown操作选择以下几种方式:
       SHUT_RD:关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
       			该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
       SHUT_WR:关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
       SHUT_RDWR:关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
       

注意:

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

端口复用

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

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

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

#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_REUSEADDR
		- SO_REUSEPORT
- optval : 端口复用的值(整形)
	- 1 : 可以复用
	- 0 : 不可以复用
- optlen : optval参数的大小
端口复用,设置的时机是在服务器绑定端口之前。

setsockopt();
bind();

演示代码, 服务器端

#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;
}

演示代码,客户端

#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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值