• LinuxC/C++ 实现简单的TCP服务端


    LinuxC/C++ 实现简单的TCP服务端



    服务器处理多个客户端请求的时候,有两种处理方式,一种是一个线程处理一个客户端请求,但是这种方式比较昂贵,现在已经弃用,另一种是使用epoll来对客户端IO进行管理.

    这篇文章我们将使用epoll来实现一个简单的TCP服务器,不过在实现之前,我们有必要先了解一下epoll.

    epoll

    epoll的内核事件表

    epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll 有很大差异. 首先,epoll 使用一组函数来完成任务,而不是单个函数. 其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像selet和poll那样每次调用都要重复传入文件描述符集或事件集. 但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表.

    创建epoll文件描述符

    #include <sys/epoll.h>
    
    int epoll_create(int size);
    
    • 1
    • 2
    • 3

    size参数现在并不起作用,只是给内核一个提示, 告诉它事件表需要多大. 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表.

    操作内核事件表

    #include <sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • 1
    • 2
    • 3

    fd参数是要操作的文件描述符,op 参数则指定操作类型。操作类型有如下3种:

    • EPOLL_CTL_ADD: 往事件表中注册fd上的事件.
    • EPOLL_CTL_MOD: 修改fd上的注册事件.
    • EPOLL_ CTL_ DEL: 删除fd上的注册事件.

    event参数指定事件,它是epoll event结构指针类型. epoll_ event的定义如下:

    struct epoll_event {
    	_uint32_t events;		// epoll事件
    	epoll_data_t data; 		// 用户数据
    };
    
    • 1
    • 2
    • 3
    • 4

    其中events成员描述事件类型. epoll 支持的事件类型和poll基本相同. 表示epoll事件类型的宏是在poll对应的宏前加上E,比如epoll的数据可读事件是EPOLLIN. 但epoll有两个额外的事件类型: EPOLLETEPOLLONESHOT. 它们对于epoll的高效运作非常关键,我们将在后面讨论它们. data 成员用于存储用户数据,其类型epoll data_t的定义如下:

    typedef union epoll_data {
    	void *ptr;
    	int fd;
    	uint32_t u32;
    	uint64_t u64;
    } epoll_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    epoll_data_t是一个联合体,其4个成员中使用最多的是fd. 它指定事件所从属的目标文件描述符.

    epoll ctl成功时返回0,失败则返回-1并设置errno.

    epoll等待事件触发

    epoll系列系统调用的主要接口是epoll_ wait函数. 它在一段超时时间内等待一组文件描述符上的事件,其原型如下:

    #include <sys/epoll.h>
    
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • 1
    • 2
    • 3

    该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno.
    关于该函数的参数,我们从后往前讨论. timeout参数的含义与poll接口的timeout参数相同. maxevents参数指定最多监听多少个事件,它必须大于0.

    epoll wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中. 这个数组只用于输出epoll wait检测到的就绪事件,而不像selectpoll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件. 这就极大地提高了应用程序索引就绪文件描述符的效率.

    LT 和 ET 模式

    epoll对文件描述符的操作有两种模式: LT (Level Trigger, 电平触发)模式ET (Edge Trigger,边沿触发)模式.

    LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll. 而当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll 将以ET模式来操作该文件描述符. ET模式是epoll的高效工作模式.

    对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件. 这样,当应用程序下一次调用epoll_wait时,cpoll_ wait还会再次向应用程序通告此事件,直到该事件被处理.
    而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件. 可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高.

    具体实现

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    #include <netinet/tcp.h>
    #include <arpa/inet.h>
    #include <pthread.h>
    
    #include <errno.h>
    #include <fcntl.h>
    
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUFFER_LENGTH		1024
    #define EPOLL_SIZE			1024
    
    int main(int argc,char* argv[]) {
        if (argc < 2) {
    		printf("Parm Error\n");
    		return -1;
    	}
    
    	int port = atoi(argv[1]);
    
    	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    	struct sockaddr_in addr;
    	memset(&addr, 0, sizeof(struct sockaddr_in));
    	addr.sin_family = AF_INET;
    	addr.sin_port = htons(port);
    	addr.sin_addr.s_addr = INADDR_ANY;
    
    	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
    		perror("bind");
    		return 2;
    	}
    
    	if (listen(sockfd, 5) < 0) {
    		perror("listen");
    		return 3;
    	}
    
        // 创建一个epoll
        int epfd = epoll_create(1);
        struct epoll_event events[EPOLL_SIZE] = {0};
    
        // 储存epoll监听的IO事件
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = sockfd;
        // 把socket交给epoll去管理
        epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
    
        while (1) {
            // epfd: 指定哪一个epoll
            // events: 指定监听事件的容器
            // EPOLL_SIZE: 数组大小
            // -1: 表示只要没有IO事件就不去处理,0表示有时间就去处理
            // 返回处理的IO事件的个数
            int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1);
            if (nready == -1) {
                continue;
            }
    
            // 依次处理IO事件
            // events容器中会储存两种fd,一种是sockfd,一种是clientfd
            int i = 0;
            for (i = 0; i < nready; i++) {
                // 触发IO事件的是sockfd,要进行accept处理
                if (events[i].data.fd == sockfd) {
                    struct sockaddr_in client_addr;
    				memset(&client_addr, 0, sizeof(struct sockaddr_in));
    				socklen_t client_len = sizeof(client_addr);
    
                    // 建立连接之后得到新的clientfd
    				int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
    
                    // 确定事件的触发方式
                    // 水平触发(有数据就触发,可能会触发多次)和边沿触发(检测到状态的改变才会触发)
                    // 这里使用边沿触发
    				ev.events = EPOLLIN | EPOLLET;
    				ev.data.fd = clientfd;
                    // clientfd交给epoll管理
    				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
                } else {
                    // 触发的是clientfd,要进行读写操作
                    int clientfd = events[i].data.fd;
    
    				char buffer[BUFFER_LENGTH] = { 0 };
    				int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
    				if (len < 0) {
    					close(clientfd);
    					ev.events = EPOLLIN | EPOLLET;
    					ev.data.fd = clientfd;
                        // 及时清除IO
    					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
    				}
    				else if (len == 0) {
    					close(clientfd);
    					ev.events = EPOLLIN | EPOLLET;
    					ev.data.fd = clientfd;
                        // 及时清除IO
    					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
    				}
    				else {
    					printf("Recv: %s, %d byte(s)\n", buffer, len);
    				}
                }
            }
    
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    CMakeLists.txt

    PROJECT(TCPSERVER)
    ADD_EXECUTABLE(tcp tcpserver.c)
    
    • 1
    • 2

    执行结果

    首先执行二进制程序:

    # 后面跟上端口号
    ./tcp 8888
    
    • 1
    • 2

    然后用NetAssist开三个客户端,向服务器发送请求:

    在这里插入图片描述
    服务端接收:

    在这里插入图片描述

    参考资料:

    《Linux高性能服务器编程》

  • 相关阅读:
    AD软件中的pcbdoc、schdoc等类似一些文件的图标变成了白板解决办法
    35岁程序员炒Luna代币千万资产3天归零;俄罗斯调查谷歌等科技公司;Linux 5.19加入了50万行图形驱动代码|极客头条
    c语言编程 结构结合(union)
    Python自动化测试面试题精选(一)
    正则表达式(Perl 示例)
    不懂就学—什么是autoML?
    localStorage和sessionStorage的使用
    Biu~送你 20 个提供远程工作的网站,都很棒
    LabVIEW大量数据的内存管理
    Java volatile功能简介说明
  • 原文地址:https://blog.csdn.net/qq_49723651/article/details/125626015