资讯详情

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

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

文章目录

  • LinuxC/C 实现简单的TCP服务端
    • epoll
    • 具体实现

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

我们将使用这篇文章epoll实现简单TCP但是,在实现之前,有必要先了解服务器epoll.

epoll

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

#include <sys/epoll.h>  int epoll_create(int size); 

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

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

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

  • 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;   // 用户数据 }; 

其中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; 

epoll_data_t它是一个联合体,最常用于四个成员fd. 指定事件所属的目标文件描述符.

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

epoll系列系统调用的主要接口是epoll_ wait函数. 其原型如下:

#incude <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

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

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

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

PROJECT(TCPSERVER)
ADD_EXECUTABLE(tcp tcpserver.c)

首先执行二进制程序:

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

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

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

参考资料:

《Linux高性能服务器编程》

标签: selet传感器b08系列

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台