前一阵子想写一个服务器,嗯,一开始是想写一个电商平台来着.............
然后就开始学,慢慢的觉得自己需要学习的东西真的还有很多很多,比如用长连接还是短连接,长连接的话怎么节省系统开销,心跳包的设置,避免产生大量小包的nagle算法,html的token怎么用,还有cookie,io复用,计时器,数不清的内部算法........So,坐下来学!
然后前天写出了这个服务器的基本的样子,昨天调试时候把bug改了改,于是一个基本模型算是出来了(嗯一个没写计时器的破模型.......最近打算把计时器写一写)。
下面写一下我的学习过程。
首先编程中通信的基本是使用套接字。套接字分为客户端和服务端。
服务端常见的流程是socket() ->bind() ->listen() ->accept()这几个函数。
(当然也有例外,比如有一种方法是客户端进行bind之后connect,这里暂时不谈。)
socket函数
socket函数指定地址描述,套接字类型,指定协议并返回一个描述符。地址描述一般来说只能选择使用AF_INET;套接字类型有tcp,udp和原始套接字比较常用,这里我使用了tcp协议,所以参数是SOCK_STREAM;最后一个参数可以不设置,我直接将其置为0。
bind函数
bind函数将刚才返回的文件描述符绑定在一个端口。并设置一个sockaddr_in变量存储服务器本身的IP和端口等信息。
listen函数
listen函数对刚才绑定的端口进行监听,并设置一个队列长度。该队列长度事实上在内核中控制了两个队列,分别是未连接队列和已连接队列。未连接队列储存的是已经发送连接请求,但是尚未完成三次握手的客户连接,当该队列中的连接完成三次握手之后,将进入已连接队列的尾部;已连接队列储存的是已经完成三次握手但是尚未被accept函数接收的客户连接。
accept函数
accept函数从已连接队列队首获取连接,将该连接的对端信息(IP和端口等)储存在一个sockaddr_in变量中,并返回一个描述符(此次客户连接的描述符)。
作为一个服务器,当然不能随意ctl+c掉,但是如果服务被关闭(比如服务器崩了)之后,立即重新启动服务,会出现一个无法绑定端口的错误。这是因为刚才的端口处在了TIME_WAIT的状态,内核中该状态将会维护两个MSL(maximum segment lifetime)的时间,linux下大约是1分钟左右,在此期间该端口不可再次绑定。
但是我们是服务器,关闭后重启的每一秒钟都很紧迫,所以当然不能让内核白白浪费这两个MSL的时间。所以我们使用setsockopt函数,对其设置端口重用。设置之后,该端口将立即可以重用。
具体方法:
setsockopt()函数有5个参数:分别是描述符,套接字接口类型,选项名称,选项值和选项名称。
我们重点关注前三个:
第一个:描述符,就是socket时候创建的那个描述符,服务描述符。
第二个:套接字接口类型,看下面这个表格:
SOL_SOCKET | 基本套接口 |
IPPROTO_IP | IPv4套接口 |
IPPROTO_IPV6 | IPv6套接口 |
IPPROTO_TCP | TCP套接口 |
我们这里使用SOL_SOCKET参数。
第三个:选项名称。这个是重点,此参数有很多选项,我们使用参数SO_REUSEADDR,也就是端口重用(好吧翻译过来是地址重用,不过不要在意这个翻译了~)。
然后第四个和第五个,指向变量的指针和该指针的空间长度,我直接设置的NULL和1。
/*伪代码*/ listen(listen_fd, MAX_QUEUE); while(1) { client_fd = accpet(listen_fd, client_addr, sizeof( sockaddr_in)); //客户连接进程 fi( fork() == 0) { close(listen_fd); while( client_request(client_fd) ); close(client); } //服务器进程 else close(client_fd); }
当然了这种架构的缺点也很明显:每个客户连接都需要分配一个独立的进程,系统开销太大,来个几千并发就挂了。
所以我们要想其他办法。
阻塞IO | 没有文件可读,则阻塞一直在此处 |
非阻塞IO | 没有文件可读,立即返回errno=EAGAIN。 |
同步IO | 数据进行读写时候,阻塞。 |
异步IO | 数据读写时候不阻塞,完成时通过事件进行通知。 |
/* set_non_blocking - 设置描述符为非阻塞 */ int set_non_blocking(int sockfd) { if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) { return -1; } return 0; }然后再来分析一下这条挺长的语句,这条语句中包涵了内外两层fcntl()函数。 我因为一开始不知道这个函数的作用,就去查了一下资料,得到函数原型是:int fcntl(int fd, int cmd, long arg / struct flock *lock),
F_GETFL | 取得文件描述词状态旗标,此旗标为open()的参数flags。 |
F_SETFL | 设置描述词状态旗标,参数arg作为新旗标,但只允许O_APPEND、 O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响 |
EPOLL_CTL_ADD | 注册新的fd到epfd中 |
EPOLL_CTL_MOD | 修改已经注册的fd的监听事件 |
EPOLL_CTL_DEL | 从epfd中删除一个fd |
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;epoll_event的成员uint32_t events的作用是储存监听的内容,可以是以下几个宏的集合:
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的。 |
EPOLLONESHOT | 只监听一次事件,当监听完结束后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。 |
/*伪代码*/ listen(); bind(); listen(); epoll_create(MAX_EVENTS); event_act = set(listen_fd, EPOLLIN | EPOLLET); //设置:边缘触发 和 可读时通知 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event_act); while(1) { get_act_fds = epoll_wait(epoll, events, curfds, -1); for(i = 0; i<get_act_fds; i++) { if(events[i] == listen_fd) { client_fd = accept(); event_act = set(client_fd, EPOLLIN | EPOLLET); epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event_act); } else { still_connect = client_request(events[n].data.fd) if(!still_connect) close(client_fd); } } }
/* * Author: [email protected] * * Created Time: 2014年05月13日 星期二 09时59分55秒 * * FileName: server.h * * Description: * */ #ifndef _SERVER_H_ #define _SERVER_H_ #include "EC_include.h" #define LENGTH 1024 #define MAXEVENTS 1024 char * server_time(); //返回服务器的本地时间 int set_non_blocking(int sockfd); //将传入的描述符设置为非阻塞 int client_request(int client_fd); //处理客户请求 char * server_time() { time_t rawtime;//服务器时间 struct tm * server_time; time(&rawtime); server_time = localtime(&rawtime); return asctime(server_time); } int set_non_blocking(int sockfd) { /* 内层调用fcntl()的F_GETFL获取flag, * 外层fcntl()将获取到的flag设置为O_NONBLOCK非阻塞*/ if( fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) ) == -1) { return -1;} return 0; } int client_request(int client_fd) { int recbytes;// 计数buffer收到的字节数[read()] char buffer[LENGTH];//存储收到的信息[read()] char server_msg[LENGTH];//发送内容长度[write()] struct sockaddr_in client_addr;//客户端地址[包括ip与端口] /*获取对端ip与端口信息*/ int len=sizeof(client_addr); getpeername( client_fd, (struct sockaddr *)&client_addr,&len ); /*read() 收取数据*/ recbytes = read(client_fd, buffer, LENGTH); /* 当recbytes > 0,正常 * 当recbytes = -1,且errno = 11,正常 * 其他情况:关闭*/ if(recbytes > 0)//当然还有[recbytes <0 && errno == EAGAIN]的情况,但不会被epoll_wait()返回到get_act_fds { buffer[recbytes]='\0'; printf("%s ",buffer); printf("from %#x : %#x : ", ntohl(client_addr.sin_addr.s_addr),ntohs(client_addr.sin_port)); printf("%s\n",server_time()); return 0; } else { /*输出断开连接的时间*/ printf(" server disconnected from %#x : %#x : ", ntohl(client_addr.sin_addr.s_addr),ntohs(client_addr.sin_port)); printf("%s\n",server_time()); close(client_fd); return -1; } } int tcp_server() { int listen_fd, //描述符:接受所有连接请求 client_fd, //描述符:处理单独的客户请求 epoll_fd, //描述符:epoll get_act_fds, //epoll_wait()返回的事件描述符数量 count_fds = 0; //epoll监控描述符计数器 struct sockaddr_in server_addr,//服务端地址 client_addr;//客户端地址 unsigned short portnum = 21567;//服务器使用端口 int sin_size;//sockaddr_in的地址长度 struct epoll_event event_act,//要监听的描述符的动作 events[MAXEVENTS];//epoll事件队列 /*设置监听的端口和IP信息*/ //bzero(&server_addr, sizeof(struct sockaddr_in)); memset(&server_addr, sizeof(struct sockaddr_in),'\0'); server_addr.sin_family=AF_INET; server_addr.sin_addr.s_addr=htonl(INADDR_ANY); server_addr.sin_port=htons(portnum); /*socket() */ listen_fd = socket(AF_INET, SOCK_STREAM,0); setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, NULL, 1);//端口复用,最后两个参数常用opt=1和sizeof(opt) if(listen_fd == -1) { printf("SOCKET FAILED "); printf("%s",server_time()); return 1; //exit(1); } printf("socket ok... "); /*bind() */ if(-1 == bind(listen_fd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))) { printf("BIND FAILED "); printf("%s",server_time()); return 1; //exit(1); } printf("bind ok... \n"); /*listen() */ if(-1 == listen(listen_fd,5)) { printf("LISTEN FAILED "); printf("%s",server_time()); return 1; //exit(1); } printf("listen ok..."); sin_size = sizeof(struct sockaddr_in); /*epoll_create() */ epoll_fd = epoll_create(MAXEVENTS); event_act.events = EPOLLIN | EPOLLET;//可读检测 + 边缘触发 event_act.data.fd = listen_fd;//设置新事件为监听描述符 /*epoll_ctl() 描述符加入监听队列*/ if( epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event_act) < 0 ) { printf("EPOLL_CTL FAILED "); printf("%s",server_time()); return 1; //exit(1); } count_fds++; /*开始循环监听 */ while(1) { /*epoll_wait() 最后一个参数-1时,当描述符可读则立即返回*/ get_act_fds = epoll_wait(epoll_fd, events, count_fds, -1); //printf("sdklskdlskdl: %d\n",get_act_fds); if(get_act_fds == -1) { printf("EPOLL_WAIT FAILED "); printf("%s",server_time()); continue; } for(int i = 0; i < get_act_fds; i++) { if(events[i].data.fd == listen_fd) { /*accept() */ if(-1 == (client_fd = accept(listen_fd,(struct sockaddr *)(&client_addr),&sin_size))) { printf("ACCEPT FAILED "); printf("%s",server_time()); continue; } /*输出连接客户的ip和端口*/ printf("accept ok... \nserver start get connect from %#x : %#x\n", ntohl(client_addr.sin_addr.s_addr),ntohs(client_addr.sin_port)); /*如果当前epoll内描述符队列已满*/ if(count_fds >= MAXEVENTS) { printf("TOO MANY CONNECTIONS\n"); continue; } /*设置非阻塞io*/ if( set_non_blocking(client_fd) != 0 ) { printf("SET_NON_BLOCKING FAILED "); printf("%s",server_time()); close(client_fd); continue; } /*设置epoll对该描述符的监听模式*/ event_act.events = EPOLLIN | EPOLLET; event_act.data.fd = client_fd; /*epoll_ctl() 描述符加入监听队列*/ if( epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event_act) <0 ) { printf("EPOLL_CTL ADD CLIENT FAILED "); printf("%s",server_time()); close(client_fd); continue; } count_fds++; continue; }//[if(events[i] == listen_fd)]结束 /*处理客户请求,若连接断开则从epoll监听队列中删除该描述符*/ else if(client_request(events[i].data.fd) < 0) { epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &event_act); count_fds--; } }//[for(i = 0; i < get_act_fds; i++)]结束 }//[while大循环]结束 close(listen_fd); } #endif