linux下实现TCP服务器的几种方式:多线程、select、poll、epoll详细过程及其思路

1. 单线程

  • 客户端
    1、socket获得本地IPV4流式套接字。
    2、初始化一个socket地址结构体存放服务端的IP地址和端口号。
    3、传入套接字地址结构体connect到服务端。
    4、从本地命令行终端输入数据到server。

代码如下

#include
#include
#include
#include
#include
#include

int main(int argc, char ** argv)
{
    if(argc < 2)
    {
        printf("please input ip and port\n");
        exit(1);
    }
    int serverfd;
    serverfd = socket(AF_INET, SOCK_STREAM, 0);
    if(serverfd < 0)
    {
        printf("socket error\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2]));
    int ret = inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr);
    if(ret < 0)
    {
        printf("inet_pton error\n");
        exit(1);
    }
    ret = connect(serverfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if(ret < 0 )
    {
        printf("connect error\n");
        exit(1);
    } 
    char buf[1024];
    while(1)
    {
        fgets(buf, sizeof(buf), stdin);
        write(serverfd, buf, strlen(buf));
    }
    close(serverfd);
    return 1;
}
  • 服务端
    1、socket获得本地IPV4流式监听套接字。
    2、初始化套接字地址结构体存放本地IP和端口。
    3、bind将监听套接字和IP、端口绑定。
    4、listen设置最大监听个数。
    5、创建套接字地址结构体用来存放请求连接的client。
    6、accept阻塞等待请求连接。

代码如下

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include  
#include 
#include 
#include 
#include 

#define MAXLNE  4096
#define POLLSIZE 1024
int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(atoi(argv[1]));
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    printf("listen...\n");

#if 0   //单线程
    struct sockaddr_in client;
    menset(&client, 0, sizeof(client));
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");
    while (1) {

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

2. 多线程

实现思路:简单。

  1. 主线程用来阻塞客户端的连接,每当有一个客户端连接就创建一个新的客户端线程。
  2. 客户端线程用于做数据处理,传入参数为客服端的socket句柄。

代码如下

/*线程*/
void* tfun(void* arg)
{
    int connfd = *(int*)arg;
    char buff[MAXLNE];
    while(1)
    {
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) 
        {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
	    } 
        else if (n == 0) 
        {
            close(connfd);
            break;
        }
    }
    pthread_exit((void*)1);
}
//main函数中
#elif 0     //多线程
    pthread_t tid;
    struct sockaddr_in clientsock;
    socklen_t len = sizeof(clientsock);
    char clie_IP[BUFSIZ];
    int i=0;
    int thread_connfd[128];
    while(1)
    {
        if((thread_connfd[i] = accept(listenfd, (struct sockaddr*)&clientsock, &len)) != -1)
        {
            printf("------client ip:%s, port:%d---:", inet_ntop(AF_INET, &clientsock.sin_addr.s_addr,
                                                                clie_IP, sizeof(clie_IP)), ntohs(clientsock.sin_port));
            pthread_create(&tid, NULL, (void*)tfun, (void*)&thread_connfd[i]);
            pthread_detach(tid);
            i++;
        }
    }

缺点:在这里插入图片描述
查看所分配线程的栈的大小:8M。32位Linux下,一个进程空间4G,内核占1G,用户留3G,一个线程默认8M,所以最多380个左右线程。当连接的用户超过一定,内存堆满会导致服务端重启。

3. select方式

select函数原型
int select(int maxfdql, fd_set *readset, fd_set *writeset, fd_set *execeptset, const struct timeval *timeout);

函数功能: 允许进程指示内核等待多个事件中的任何一个发生,并只有一个或者多个事件发生或经历一段指定的事件才唤醒它。

maxfdql:指定待测试的描述符个数,传入的值应该是待测试最大描述符加一。

fd_set类型:实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系。

建立联系的宏

  1. void FD_ZERO(fd_set *fdset); //清空(初始化)描述符集的所有位
  2. void FD_SET(int fd, fd_set *fdset); //打开描述符集的fd位
  3. void FD_CLR(int fd, fd_set *fdset); //关闭描述符集的fd位
  4. int FD_ISSET(int fd, fd_set *fdset); //查看fd是否被置位

readset:传入传出的可读事件描述符集合。
writeset:传入传出的可写事件描述符集合。
exceptset:传入传出的异常事件描述符集合。

const struct timeval *timeout:告知内核等待所指定描述符的任何一个就绪可以花多长时间,具体看UNP。
有三种可能:
1、永久等下去,阻塞但并不占用cpu。
2、等待一段固定时间。
3、根本不等待,轮询方式。

返回值:监听到事件已经发生的描述符个数。

select服务器实现思路

  1. 将监听套接字放入可读事件描述符集,传入select进行监听。
  2. 当有新的请求连接时候,FD_ISSET查看刻度事件描述符集中监听套接字所在的位会被置位,这时候调用accept处理连接,并将新的连接套接字FD_SET加入到可读事件描述符中。
  3. 当有客户端发送数据过来时, FD_ISSET遍历检查可读事件描述符集找到客户端并recv进行接收处理。

代码如下

#elif 0     //select实现
    fd_set rset, rfds;
    FD_ZERO(&rset);
    FD_SET(listenfd, &rset);

    char clie_IP[BUFSIZ];
    int Numready;
    int maxfd = listenfd;
    while(1)
    {
        rfds = rset;
        Numready = select(maxfd+1, &rfds, NULL, NULL, NULL);
        struct sockaddr_in sockclient;
        socklen_t len = sizeof(sockclient);
        if(FD_ISSET(listenfd, &rfds))
        {
            if((connfd = accept(listenfd,(struct sockaddr*)&sockclient, &len)) == -1)
            {
                printf("accept error\n");
                exit(1);
            }
            printf("------connfd:%d,client ip:%s, port:%d---:\n", connfd,inet_ntop(AF_INET, &sockclient.sin_addr.s_addr,
                                                                    clie_IP, sizeof(clie_IP)), ntohs(sockclient.sin_port));
            FD_SET(connfd, &rset);
            if(connfd > maxfd)maxfd = connfd;
            if(--Numready == 0)continue;
        }
        for(int i = listenfd; i<=maxfd; i++)
        {
            if(FD_ISSET(i, &rfds))
            {
                n = recv(i, buff, sizeof(buff), 0);
                if(n>0)
                {
                    buff[n] = '\0';
                printf("msg from client[%d]%s\n", i,buff);
                }
                else if(n == 0)
                {
                    FD_CLR(i, &rfds);
                    printf("%d disconnected\n", i);
                    close(i);
                }
                if(--Numready == 0)break;
            }   
        }
    }

4. poll方式

poll函数原型
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

struct pollfd{
int fd; //fd to check(还是英文直观)
short events; //对于fd需要检查的事件,输入值
short revents; //检查到的事件,返回值
}

nfds:指定待测试的描述符个数,传入的值应该是待测试最大描述符加一。

timeout:有三种可能:
1、永久等下去,阻塞但并不占用cpu。
2、等待一段固定时间。
3、根本不等待,轮询方式。

poll服务器实现思路
和select类似,不过poll体现在结构体上。

  1. 将监听套接字描述符listenfd放入pollfd数组第一位中,并传入POLLIN事件进行检测。
  2. 当有连接时候,listenfd所在的pollfd数组位置的revents就会有事件返回,根据这点来调用accept处理连接请求,完成连接之后,将连接进来的新的客户端描述符放入pollfd数组中,并设置检测事件。
  3. 当有客户端发送来数据,遍历一遍pollfd数组,查看是哪个客户端的描述符有事件发生。

代码如下

#elif 0   //poll实现
    struct pollfd pollarrfd[POLLSIZE] = {0};
    int maxfd = listenfd;
    char clie_IP[BUFSIZ];
    pollarrfd[0].fd = listenfd;
    pollarrfd[0].events = POLLIN;
    struct sockaddr_in sockclient;
    socklen_t len = sizeof(sockclient);
    while(1)
    {
        int Numready = poll(pollarrfd, maxfd+1, -1);
        if(Numready < 0)
        {
            printf("poll error\n");
            exit(1);
        }
        if(pollarrfd[0].revents & POLLIN)
        {
            connfd = accept(listenfd, (struct sockaddr*)&sockclient, &len);
            if(connfd < 0)
            {
                printf("accept error\n");
                exit(1);
            }
            printf("accept\n");
            printf("------connfd:%d,client ip:%s, port:%d---:\n", connfd, inet_ntop(AF_INET, &sockclient.sin_addr.s_addr,
                                                                        clie_IP, sizeof(clie_IP)), ntohs(sockclient.sin_port));
            pollarrfd[connfd].fd = connfd;
            pollarrfd[connfd].events = POLLIN;
            if(connfd > maxfd)maxfd = connfd;
            if(--Numready == 0)continue;
        }
        for(int i = 1; i<=maxfd; i++)
        {
            if(pollarrfd[i].revents & POLLIN)
            {
                n = recv(i, buff, sizeof(buff), 0);
                if(n<0)
                {
                    printf("recv error\n");
                    exit(1);
                }
                if(n>0)
                {
                    buff[n] = '\0';
                    printf("msg from client[%d]:%s\n", i, buff);
                }
                if(n==0)
                {
                    close(i);
                    printf("socket[%d] close\n", i);
                }
            }
        }
    }

5. epoll方式

函数接口
int epoll_create(int size)
size:监听个数。 返回值:epoll句柄。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epfd:epoll_create创建的epoll句柄。

op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);

event: 告诉内核需要监听的事件

struct epoll_event {
__uint32_t events; /*Epoll events */
epoll_data_t data; /*User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

	EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
	EPOLLOUT:	表示对应的文件描述符可以写
	EPOLLPRI:	表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
	EPOLLERR:	表示对应的文件描述符发生错误
	EPOLLHUP:	表示对应的文件描述符被挂断;
	EPOLLET: 	将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
	EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

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

epfd:epoll句柄。

events:返回值,数组里储存了所有触发事件的文件描述符和事件类型。

maxevents:通知内核这个events多大。

timeout:等待时间
1、-1:阻塞。
2、0:非阻塞。
3、>0:指定毫秒。

epoll实现思路

  1. epoll_create创建epollfd,epoll_ctl添加listenfd进去,并设置其event为event为EPOLLIN,data为listenfd。
  2. 创建一个event接收epoll_wait等待epfd下的描述符发生的事件。
  3. 当事件产生时,根据epoll_wait返回来的事件个数遍历传入epoll_wait的events,从而获取发生事件的描述符和事件类型。
  4. 根据事件描述符和事件类型,判断是客户连接还是客户端的数据发送,并作对应处理。

代码如下

#else   //epoll实现
    int Numready;
    char clie_IP[BUFSIZ];
    struct sockaddr_in sockclient;
    socklen_t len = sizeof(sockclient);
    int epollfd = epoll_create(1);
    if(epollfd < 0 )
    {
        printf("epoll_create error\n");
        exit(1);
    }
    struct epoll_event event[POLLSIZE];
    struct epoll_event ev;

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    int ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
    if(ret < 0)
    {
        printf("epoll_ctl error\n");
        exit(1);
    }
    while(1)
    {
        Numready = epoll_wait(epollfd, event, POLLSIZE, -1);
        if(Numready < 0)
        {
            printf("epoll_wait error\n");
            exit(1);
        }
        for(int i=0; i<Numready; i++)
        {
            if(event[i].data.fd == listenfd)
            {
                connfd = accept(listenfd, (struct sockaddr*)&sockclient, &len);
                if(connfd < 0)
                {
                    printf("accept error\n");
                    exit(1);
                }
                printf("------connfd:%d,client ip:%s, port:%d---:\n", connfd, inet_ntop(AF_INET, &sockclient.sin_addr.s_addr,
                                                                        clie_IP, sizeof(clie_IP)), ntohs(sockclient.sin_port));
                
                event[i].data.fd = connfd;
                event[i].events = EPOLLIN;
                ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, event);
                if(ret < 0)
                {
                    printf("epoll_ctl_add error\n");
                    exit(1);
                }
                continue;
            }
            else if(event[i].events & EPOLLIN)
            {
                int clientfd = event[i].data.fd;
                n = recv(clientfd, buff, sizeof(buff), 0);
                if(n < 0 )
                {
                    printf("recv error\n");
                    exit(1);
                }
                if(n>0)
                {
                    buff[n] = '\0';
                    printf("msg from client[%d]:%s\n", clientfd, buff);
                }
                if(n==0)
                {
                    close(clientfd);
                    printf("client[%d] close\n", clientfd);
                }
            }  
        }
    }

#endif

总结

多线程:实现方法简单,阻塞IO,但是浪费资源,可用来实现少量客户的会议室服务器。
select:配合fd_set位图及其几个宏使用,可对读、写、异常事件非阻塞保留事件,交给程序员遍历查询。
poll:和select很相像,但是poll使用pollfd结构体传入和传出来完成,而且事件类型比select多,接口也没有select多,可以直接通过结构体来判断。
epoll:接口较多,实现的方式比较多,虽然相对复杂但是性能和实现功能上更好。无须遍历侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。目前epell是linux大规模并发网络程序中的热门首选模型。

全部代码自行git clone https://github.com/qiushii/server.git
具体内容,请参考《UNP》《APUE》。

你可能感兴趣的:(服务器,linux,tcp/ip)