网站被k表现,wordpress 关闭插件更新,wordpress php 版本,深圳网上申请营业执照流程http://blog.csdn.net/qq_31564375/article/details/51581038项目介绍
本项目是实现一个简单的聊天室#xff0c;聊天室分为服务端和客户端。本项目将很多复杂的功能都去掉了#xff0c;线程池、多线程编程、超时重传、确认收包等等都不会涉及。总共300多行代码#xff0c;让…http://blog.csdn.net/qq_31564375/article/details/51581038项目介绍
本项目是实现一个简单的聊天室聊天室分为服务端和客户端。本项目将很多复杂的功能都去掉了线程池、多线程编程、超时重传、确认收包等等都不会涉及。总共300多行代码让大家真正了解C/S模型以及epoll的使用。为了方便查看代码已经改的很小白绝对比nginx源码好理解(当然大家有兴趣的话还是要拜读下nginx源码绝对大有收获)。希望本项目能为大家以后的工作或者学习提供一点帮助 介绍如下
1. 服务端
a. 支持多个用户接入实现聊天室的基本功能
b. 使用epoll机制实现并发增加效率
2. 客户端
a. 支持用户输入聊天消息
b. 显示其他用户输入的信息
c. 使用fork创建两个进程
子进程有两个功能
等待用户输入聊天信息将聊天信息写到管道pipe并发送给父进程
父进程有两个功能
使用epoll机制接受服务端发来的信息并显示给用户使用户看到其他用户的聊天信息将子进程发给的聊天信息从管道pipe中读取, 并发送给服务端
3. 代码说明
一共有3个文件, 即 server.cpp, client.cpp, utility.h
a. server.cpp是服务端程序
b. client.cpp是客户端程序
c. utility.h是一个头文件包含服务端程序和客户端程序都会用到的一些头文件、变量声明、函数、宏等。1.1 TCP服务端通信的常规步骤
(1)使用socket()创建TCP套接字socket
(2)将创建的套接字绑定到一个本地地址和端口上Bind
(3)将套接字设为监听模式准备接收客户端请求listen
(4)等待客户请求到来: 当请求到来后接受连接请求返回一个对应于此次连接的新的套接字accept
(5)用accept返回的套接字和客户端进行通信使用write()/send()或send()/recv() )
(6)返回等待另一个客户请求
(7)关闭套接字
[cpp] view plain copy//server.cpp代码通信模块 //服务端地址 ip地址 端口号 struct sockaddr_in serverAddr; serverAddr.sin_family PF_INET; serverAddr.sin_port htons(SERVER_PORT); serverAddr.sin_addr.s_addr inet_addr(SERVER_HOST); //服务端创建监听socket int listener socket(PF_INET, SOCK_STREAM, 0); if(listener 0) { perror(listener); exit(-1);} printf(listen socket created \n); //将服务端地址与监听socket绑定 if( bind(listener, (struct sockaddr *)serverAddr, sizeof(serverAddr)) 0) { perror(bind error); exit(-1); } //开始监听 int ret listen(listener, 5); if(ret 0) { perror(listen error); exit(-1);} printf(Start to listen: %s\n, SERVER_HOST); 2. 基本技术介绍
2.1 阻塞与非阻塞socket通常的对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞方式。
1. 阻塞方式是指 当试图对该文件描述符进行读写时如果当时没有数据可读或者暂时不可写程序就进入等待状态直到有东西可读或者可写为止。
2. 非阻塞方式是指 如果没有数据可读或者不可写读写函数马上返回而不会等待。
3. 举个例子来说比如说小明去找一个女神聊天女神却不在。 如果小明舍不得走只能在女神大门口死等着当然小明可以休息。当女 神来了她会把你唤醒囧因为挡着她门了这就是阻塞方式。如果小明发现女神不在立即离开以后每隔十分钟回来看一下采用轮询方式不在的话仍然立即离开这就是非阻塞方式。
4. 阻塞方式和非阻塞方式唯一的区别 是否立即返回。本项目采用更高效的做法所以应该将socket设置为非阻塞方式。这样能充分利用服务器资源效率得到了很大提高。[cpp] view plain copy//utility.h代码设置非阻塞函数模块 //将文件描述符设置为非阻塞方式利用fcntl函数 int setnonblocking(int sockfd) { fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK); return 0; } 2.2 epoll前面介绍了阻塞和非阻塞方式现在该介绍下epoll机制了。epoll真的是一个特别重要的概念实验的师兄们去bat任何一家面试后台开发或者系统开发等相关职位都会问epoll机制。当服务端的在线人数越来越多会导致系统资源吃紧I/O效率越来越慢这时候就应该考虑epoll了。epoll是Linux内核为处理大批句柄而作改进的poll是Linux特有的I/O函数。其特点如下
a.
epoll是Linux下多路复用IO接口select/poll的增强版本。其实现和使用方式与select/poll有很多不同epoll通过一组函数来完成有关任务而不是一个函数。b.epoll之所以高效是因为epoll将用户关心的文件描述符放到内核里的一个事件表中而不是像select/poll每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生比如说读事件epoll无须遍历整个被侦听的描述符集只要遍历那些被内核IO事件异步唤醒而加入就绪队列的描述符集合就行了。c.epoll有两种工作方式LT(level triggered)水平触发和ET(edge-triggered)边沿触发。LT是select/poll使用的触发方式比较低效而ET是epoll的高速工作方式本项目使用epoll的ET方式。d.通俗理解就是比如说有一堆女孩有的很漂亮有的很凤姐。现在你想找漂亮的女孩聊天LT就是你需要把这一堆女孩全都看一遍才可以找到其中的漂亮的就绪事件而ET是你的小弟内核将N个漂亮的女孩编号告诉你你直接去看就好所以epoll很高效。另外还记得小明找女神聊天的例子吗采用非阻塞方式小明还需要每隔十分钟回来看一下select如果小明有小弟内核帮他守在大门口女神回来了小弟会主动打电话告诉小明女神回来了快来处理吧这就是epoll。epoll 共3个函数,1、int epoll_create(int size)创建一个epoll句柄参数size用来告诉内核监听的数目size为epoll所支持的最大句柄数2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)函数功能 epoll事件注册函数参数epfd为epoll的句柄即epoll_create返回值参数op表示动作用3个宏来表示 EPOLL_CTL_ADD(注册新的fd到epfd) EPOLL_CTL_MOD(修改已经注册的fd的监听事件)EPOLL_CTL_DEL(从epfd删除一个fd)其中参数fd为需要监听的标示符参数event告诉内核需要监听的事件event的结构如下struct epoll_event {__uint32_t events; //Epoll eventsepoll_data_t data; //User data variable};其中介绍events是宏的集合本项目主要使用EPOLLIN(表示对应的文件描述符可以读即读事件发生)其他宏类型可以google之3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生函数返回需要处理的事件数目该数目是就绪事件的数目就是前面所说漂亮女孩的个数N因此服务端使用epoll的时候步骤如下调用epoll_create函数在Linux内核中创建一个事件表然后将文件描述符监听套接字listener添加到所创建的事件表中在主循环中调用epoll_wait等待返回就绪的文件描述符集合分别处理就绪的事件集合本项目中一共有两类事件新用户连接事件和用户发来消息事件epoll还有很多其他事件本项目为简洁明了不介绍。下面介绍下如何将一个socket添加到内核事件表中如下[cpp] view plain copy//utility.h添加socket模块 //将文件描述符fd添加到epollfd标示的内核事件表中 并注册EPOLLIN和EPOOLET事件EPOLLIN是数据可读事件EPOOLET表明是ET工作方式。最后将文件描述符设置非阻塞方式 /** * param epollfd: epoll句柄 * param fd: 文件描述符 * param enable_et : enable_et true, 采用epoll的ET工 作方式否则采用LT工作方式 **/ void addfd( int epollfd, int fd, bool enable_et ) { struct epoll_event ev; ev.data.fd fd; ev.events EPOLLIN; if( enable_et ) ev.events EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, ev); setnonblocking(fd); printf(fd added to epoll!\n\n); } 3. 服务端实现上面我们介绍了基本的模型和技术现在该去实现服务端了。首先介绍下utility.h中一些变量和函数。3.1 utility.h[cpp] view plain copy/* 限于篇幅这里先介绍下utility.h的主要构成。其中的头文件和一些函数实现没有显示完整源码位于3.2节 */ //服务端存储所有在线用户socket, 便于广播信息 listint clients_list; // 服务器ip地址为测试使用本地机地址可以更改为其他服务端地址 #define SERVER_IP 127.0.0.1 // 服务器端口号 #define SERVER_PORT 8888 //int epoll_create(int size)中的size为epoll支持的最大句柄数 #define EPOLL_SIZE 5000 // 缓冲区大小65535 #define BUF_SIZE 0xFFFF //一些宏 #define SERVER_WELCOME Welcome you join to the chat room! Your chat ID is: Client #%d #define SERVER_MESSAGE ClientID %d say %s #define EXIT EXIT #define CAUTION There is only one int the char room! /* 一些函数 */ //设置非阻塞 int setnonblocking(int sockfd) //将文件描述符fd添加到epollfd标示的内核事件表 void addfd( int epollfd, int fd, bool enable_et ) //服务端发送广播信息使所有用户都能收到消息 int sendBroadcastmessage(int clientfd) 3.1 utility.h完整源码 #ifndef UTILITY_H_INCLUDED #define UTILITY_H_INCLUDED #include iostream #include list #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/epoll.h #include fcntl.h #include errno.h #include unistd.h #include stdio.h #include stdlib.h #include string.h using namespace std; // clients_list save all the clientss socket listint clients_list; /********************** macro defintion **************************/ // server ip #define SERVER_IP 127.0.0.1 // server port #define SERVER_PORT 8888 //epoll size #define EPOLL_SIZE 5000 //message buffer size #define BUF_SIZE 0xFFFF #define SERVER_WELCOME Welcome you join to the chat room! Your chat ID is: Client #%d #define SERVER_MESSAGE ClientID %d say %s // exit #define EXIT EXIT #define CAUTION There is only one int the char room! /********************** some function **************************/ /** * param sockfd: socket descriptor * return 0 **/ int setnonblocking(int sockfd) { fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK); return 0; } /** * param epollfd: epoll handle * param fd: socket descriptor * param enable_et : enable_et true, epoll use ET; otherwise LT **/ void addfd( int epollfd, int fd, bool enable_et ) { struct epoll_event ev; ev.data.fd fd; ev.events EPOLLIN; if( enable_et ) ev.events EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, ev); setnonblocking(fd); printf(fd added to epoll!\n\n); } /** * param clientfd: socket descriptor * return : len **/ int sendBroadcastmessage(int clientfd) { // buf[BUF_SIZE] receive new chat message // message[BUF_SIZE] save format message char buf[BUF_SIZE], message[BUF_SIZE]; bzero(buf, BUF_SIZE); bzero(message, BUF_SIZE); // receive message printf(read from client(clientID %d)\n, clientfd); int len recv(clientfd, buf, BUF_SIZE, 0); if(len 0) // len 0 means the client closed connection { close(clientfd); clients_list.remove(clientfd); //server remove the client printf(ClientID %d closed.\n now there are %d client in the char room\n, clientfd, (int)clients_list.size()); } else //broadcast message { if(clients_list.size() 1) { // this means There is only one int the char room send(clientfd, CAUTION, strlen(CAUTION), 0); return len; } // format message to broadcast sprintf(message, SERVER_MESSAGE, clientfd, buf); listint::iterator it; for(it clients_list.begin(); it ! clients_list.end(); it) { if(*it ! clientfd){ if( send(*it, message, BUF_SIZE, 0) 0 ) { perror(error); exit(-1);} } } } return len; } #endif // UTILITY_H_INCLUDED 3.3 服务端完整源码在上面的基础上。服务端的代码就很容易写出了[cpp] view plain copy#include utility.h int main(int argc, char *argv[]) { //服务器IP port struct sockaddr_in serverAddr; serverAddr.sin_family PF_INET; serverAddr.sin_port htons(SERVER_PORT); serverAddr.sin_addr.s_addr inet_addr(SERVER_IP); //创建监听socket int listener socket(PF_INET, SOCK_STREAM, 0); if(listener 0) { perror(listener); exit(-1);} printf(listen socket created \n); //绑定地址 if( bind(listener, (struct sockaddr *)serverAddr, sizeof(serverAddr)) 0) { perror(bind error); exit(-1); } //监听 int ret listen(listener, 5); if(ret 0) { perror(listen error); exit(-1);} printf(Start to listen: %s\n, SERVER_IP); //在内核中创建事件表 int epfd epoll_create(EPOLL_SIZE); if(epfd 0) { perror(epfd error); exit(-1);} printf(epoll created, epollfd %d\n, epfd); static struct epoll_event events[EPOLL_SIZE]; //往内核事件表里添加事件 addfd(epfd, listener, true); //主循环 while(1) { //epoll_events_count表示就绪事件的数目 int epoll_events_count epoll_wait(epfd, events, EPOLL_SIZE, -1); if(epoll_events_count 0) { perror(epoll failure); break; } printf(epoll_events_count %d\n, epoll_events_count); //处理这epoll_events_count个就绪事件 for(int i 0; i epoll_events_count; i) { int sockfd events[i].data.fd; //新用户连接 if(sockfd listener) { struct sockaddr_in client_address; socklen_t client_addrLength sizeof(struct sockaddr_in); int clientfd accept( listener, ( struct sockaddr* )client_address, client_addrLength ); printf(client connection from: %s : % d(IP : port), clientfd %d \n, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port), clientfd); addfd(epfd, clientfd, true); // 服务端用list保存用户连接 clients_list.push_back(clientfd); printf(Add new clientfd %d to epoll\n, clientfd); printf(Now there are %d clients int the chat room\n, (int)clients_list.size()); // 服务端发送欢迎信息 printf(welcome message\n); char message[BUF_SIZE]; bzero(message, BUF_SIZE); sprintf(message, SERVER_WELCOME, clientfd); int ret send(clientfd, message, BUF_SIZE, 0); if(ret 0) { perror(send error); exit(-1); } } //处理用户发来的消息并广播使其他用户收到信息 else { int ret sendBroadcastmessage(sockfd); if(ret 0) { perror(error);exit(-1); } } } } close(listener); //关闭socket close(epfd); //关闭内核 return 0; } g server.cpp utility.h -o server./server4. 客户端实现4.1 子进程和父进程的通信前面已经介绍了子进程和父进程的功能他们之间用管道进行通信。如下图所示我们可以更直观的了解子进程和父进程各自的功能。 通过调用int pipe(int fd[2])函数创建管道, 其中fd[0]用于父进程读 fd[1]用于子进程写。[cpp] view plain copy//client.cpp代码管道模块 // 创建管道. int pipe_fd[2]; if(pipe(pipe_fd) 0) { perror(pipe error); exit(-1); } 通过int pid fork()函数创建子进程当pid 0 错误当pid 0, 说明是子进程当pid 0说明是父进程。根据pid的值我们可以父子进程从而实现对应的功能4.2 客户端完整源码根据上述介绍我们可以写出客户端的源码。如下[cpp] view plain copy#include utility.h int main(int argc, char *argv[]) { //用户连接的服务器 IP port struct sockaddr_in serverAddr; serverAddr.sin_family PF_INET; serverAddr.sin_port htons(SERVER_PORT); serverAddr.sin_addr.s_addr inet_addr(SERVER_IP); // 创建socket int sock socket(PF_INET, SOCK_STREAM, 0); if(sock 0) { perror(sock error); exit(-1); } // 连接服务端 if(connect(sock, (struct sockaddr *)serverAddr, sizeof(serverAddr)) 0) { perror(connect error); exit(-1); } // 创建管道其中fd[0]用于父进程读fd[1]用于子进程写 int pipe_fd[2]; if(pipe(pipe_fd) 0) { perror(pipe error); exit(-1); } // 创建epoll int epfd epoll_create(EPOLL_SIZE); if(epfd 0) { perror(epfd error); exit(-1); } static struct epoll_event events[2]; //将sock和管道读端描述符都添加到内核事件表中 addfd(epfd, sock, true); addfd(epfd, pipe_fd[0], true); // 表示客户端是否正常工作 bool isClientwork true; // 聊天信息缓冲区 char message[BUF_SIZE]; // Fork int pid fork(); if(pid 0) { perror(fork error); exit(-1); } else if(pid 0) // 子进程 { //子进程负责写入管道因此先关闭读端 close(pipe_fd[0]); printf(Please input exit to exit the chat room\n); while(isClientwork){ bzero(message, BUF_SIZE); fgets(message, BUF_SIZE, stdin); // 客户输出exit,退出 if(strncasecmp(message, EXIT, strlen(EXIT)) 0){ isClientwork 0; } // 子进程将信息写入管道 else { if( write(pipe_fd[1], message, strlen(message) - 1 ) 0 ) { perror(fork error); exit(-1); } } } } else //pid 0 父进程 { //父进程负责读管道数据因此先关闭写端 close(pipe_fd[1]); // 主循环(epoll_wait) while(isClientwork) { int epoll_events_count epoll_wait( epfd, events, 2, -1 ); //处理就绪事件 for(int i 0; i epoll_events_count ; i) { bzero(message, BUF_SIZE); //服务端发来消息 if(events[i].data.fd sock) { //接受服务端消息 int ret recv(sock, message, BUF_SIZE, 0); // ret 0 服务端关闭 if(ret 0) { printf(Server closed connection: %d\n, sock); close(sock); isClientwork 0; } else printf(%s\n, message); } //子进程写入事件发生父进程处理并发送服务端 else { //父进程从管道中读取数据 int ret read(events[i].data.fd, message, BUF_SIZE); // ret 0 if(ret 0) isClientwork 0; else{ // 将信息发送给服务端 send(sock, message, BUF_SIZE, 0); } } }//for }//while } if(pid){ //关闭父进程和sock close(pipe_fd[0]); close(sock); }else{ //关闭子进程 close(pipe_fd[1]); } return 0; } 同理建立一个client.cpp文件并将上述完整源码拷贝进去然后启动一个新的XFce终端执行如下命令cd Desktop
g client.cpp utility.h -o client
./client如图所示通过查看两个终端界面可以看到有一个用户登陆服务端了。 同理再点击一下桌面上的XFce开启一个终端运行同样的命令这里不用运行g进行编译了因为前面已经生成了可执行文件clientcd Desktop
./client转载自https://www.shiyanlou.com/courses/315很好的学习了epoll。