1.服务端

a. 支持多个用户接入,实现聊天室的基本功能

b. 使用epoll机制实现并发,增加效率

2. 客户端

a. 支持用户输入聊天消息

b. 显示其他用户输入的信息

c. 使用fork创建两个进程

子进程有两个功能:

等待用户输入聊天信息
将聊天信息写到管道(pipe),并发送给父进程

父进程有两个功能

使用epoll机制接受服务端发来的信息,并显示给用户,使用户看到其他用户的聊天信息
将子进程发给的聊天信息从管道(pipe)中读取, 并发送给服务端

C/S模型

服务端和客户端采用经典的C/S模型,并且使用TCP连接.

TCP服务端通信的常规步骤

(1)使用socket()创建TCP套接字(socket)

(2)将创建的套接字绑定到一个本地地址和端口上(Bind)

(3)将套接字设为监听模式,准备接收客户端请求(listen)

(4)等待客户请求到来: 当请求到来后,接受连接请求,返回一个对应于此次连接的新的套接字(accept)

(5)用accept返回的套接字和客户端进行通信(使用write()/send()或send()/recv() )

(6)返回,等待另一个客户请求

(7)关闭套接字

TCP客户端通信的常规步骤

(1)创建套接字(socket)

(2)使用connect()建立到达服务器的连接(connect)

(3)客户端进行通信(使用write()/send()或send()/recv())

(4)使用close()关闭客户连接

阻塞与非阻塞socket

通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞方式。

(1). 阻塞方式是指: 当试图对该文件描述符进行读写时,如果当时没有数据可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。

(2). 非阻塞方式是指: 如果没有数据可读,或者不可写,读写函数马上返回,而不会等待。

阻塞方式和非阻塞方式唯一的区别: 是否立即返回。本项目采用更高效的做法,所以应该将socket设置为非阻塞方式。这样能充分利用服务器资源,效率得到了很大提高。

epoll

当服务端的在线人数越来越多,会导致系统资源吃紧,I/O效率越来越慢,这时候就应该考虑epoll了。epoll是Linux内核为处理大批句柄而作改进的poll,是Linux特有的I/O函数。其特点如下:

epoll是Linux下多路复用IO接口select/poll的增强版本。其实现和使用方式与select/poll有很多不同,epoll通过一组函数来完成有关任务,而不是一个函数。

epoll之所以高效,是因为epoll将用户关心的文件描述符放到内核里的一个事件表中,而不是像select/poll每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入就绪队列的描述符集合就行了。

epoll有两种工作方式,LT(level triggered):水平触发和ET(edge-triggered):边沿触发。LT是select/poll使用的触发方式,比较低效;而ET是epoll的高速工作方式(本项目使用epoll的ET方式)。

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 events
epoll_data_t data; //User data variable
};
其中介绍events是宏的集合,本项目主要使用EPOLLIN(表示对应的文件描述符可以读,即读事件发生)

3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,函数返回需要处理的事件数目(该数目是就绪事件的数目,就是前面所说漂亮女孩的个数N)

因此服务端使用epoll的时候,步骤如下:

调用epoll_create函数在Linux内核中创建一个事件表;
然后将文件描述符(监听套接字listener)添加到所创建的事件表中;
在主循环中,调用epoll_wait等待返回就绪的文件描述符集合;
分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件

服务端实现

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 clients's socket
list<int> 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, )| O_NONBLOCK);
return ;
} /**
* @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, ); if(len == ) // 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() == ) { // this means There is only one int the char room
send(clientfd, CAUTION, strlen(CAUTION), );
return len;
}
// format message to broadcast
sprintf(message, SERVER_MESSAGE, clientfd, buf); list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it != clientfd){
if( send(*it, message, BUF_SIZE, ) < ) { perror("error"); exit(-);}
}
}
}
return len;
}
#endif // UTILITY_H_INCLUDED

服务端完整源码

 #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, );
if(listener < ) { perror("listener"); exit(-);}
printf("listen socket created \n");
//绑定地址
if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < ) {
perror("bind error");
exit(-);
}
//监听
int ret = listen(listener, );
if(ret < ) { perror("listen error"); exit(-);}
printf("Start to listen: %s\n", SERVER_IP);
//在内核中创建事件表
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < ) { perror("epfd error"); exit(-);}
printf("epoll created, epollfd = %d\n", epfd);
static struct epoll_event events[EPOLL_SIZE];
//往内核事件表里添加事件
addfd(epfd, listener, true);
//主循环
while()
{
//epoll_events_count表示就绪事件的数目
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -);
if(epoll_events_count < ) {
perror("epoll failure");
break;
} printf("epoll_events_count = %d\n", epoll_events_count);
//处理这epoll_events_count个就绪事件
for(int i = ; 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, );
if(ret < ) { perror("send error"); exit(-); }
}
//处理用户发来的消息,并广播,使其他用户收到信息
else
{
int ret = sendBroadcastmessage(sockfd);
if(ret < ) { perror("error");exit(-); }
}
}
}
close(listener); //关闭socket
close(epfd); //关闭内核
return ;
}

客户端实现

子进程和父进程的通信

通过调用int pipe(int fd[2])函数创建管道, 其中fd[0]用于父进程读, fd[1]用于子进程写。

通过int pid = fork()函数,创建子进程,当pid < 0 错误;当pid = 0, 说明是子进程;当pid > 0说明是父进程。根据pid的值,我们可以父子进程,从而实现对应的功能!

客户端完整源码

 #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, );
if(sock < ) { perror("sock error"); exit(-); }
// 连接服务端
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < ) {
perror("connect error");
exit(-);
} // 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
int pipe_fd[];
if(pipe(pipe_fd) < ) { perror("pipe error"); exit(-); } // 创建epoll
int epfd = epoll_create(EPOLL_SIZE);
if(epfd < ) { perror("epfd error"); exit(-); }
static struct epoll_event events[];
//将sock和管道读端描述符都添加到内核事件表中
addfd(epfd, sock, true);
addfd(epfd, pipe_fd[], true);
// 表示客户端是否正常工作
bool isClientwork = true; // 聊天信息缓冲区
char message[BUF_SIZE]; // Fork
int pid = fork();
if(pid < ) { perror("fork error"); exit(-); }
else if(pid == ) // 子进程
{
//子进程负责写入管道,因此先关闭读端
close(pipe_fd[]);
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)) == ){
isClientwork = ;
}
// 子进程将信息写入管道
else {
if( write(pipe_fd[], message, strlen(message) - ) < )
{ perror("fork error"); exit(-); }
}
}
}
else //pid > 0 父进程
{
//父进程负责读管道数据,因此先关闭写端
close(pipe_fd[]); // 主循环(epoll_wait)
while(isClientwork) {
int epoll_events_count = epoll_wait( epfd, events, , - );
//处理就绪事件
for(int i = ; i < epoll_events_count ; ++i)
{
bzero(&message, BUF_SIZE); //服务端发来消息
if(events[i].data.fd == sock)
{
//接受服务端消息
int ret = recv(sock, message, BUF_SIZE, ); // ret= 0 服务端关闭
if(ret == ) {
printf("Server closed connection: %d\n", sock);
close(sock);
isClientwork = ;
}
else printf("%s\n", message); }
//子进程写入事件发生,父进程处理并发送服务端
else {
//父进程从管道中读取数据
int ret = read(events[i].data.fd, message, BUF_SIZE); // ret = 0
if(ret == ) isClientwork = ;
else{ // 将信息发送给服务端
send(sock, message, BUF_SIZE, );
}
}
}//for
}//while
} if(pid){
//关闭父进程和sock
close(pipe_fd[]);
close(sock);
}else{
//关闭子进程
close(pipe_fd[]);
}
return ;
}

epoll聊天室的实现的更多相关文章

  1. 20分钟了解Epoll + 聊天室实战

    我们知道,计算机的硬件资源由操作系统管理.调度,我们的应用程序运行在操作系统之上,我们的程序运行需要访问计算机上的资源(如读取文件,接收网络请求),操作系统有内核空间和用户空间之分,所以数据读取,先由 ...

  2. 使用epoll实现聊天室功能,同时比较epoll和select的异同

    1.首先介绍一下select和epoll的异同,如下(摘抄自https://www.cnblogs.com/Anker/p/3265058.html) select的几大缺点: (1)每次调用sele ...

  3. 基于EPOLL模型的局域网聊天室和Echo服务器

    一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...

  4. C++ socket 网络编程 简单聊天室

    操作系统里的进程通讯方式有6种:(有名/匿名)管道.信号.消息队列.信号量.内存(最快).套接字(最常用),这里我们来介绍用socket来实现进程通讯. 1.简单实现一个单向发送与接收 这是套接字的工 ...

  5. 利用Node.js的Net模块实现一个命令行多人聊天室

    1.net模块基本API 要使用Node.js的net模块实现一个命令行聊天室,就必须先了解NET模块的API使用.NET模块API分为两大类:Server和Socket类.工厂方法. Server类 ...

  6. php+websocket搭建简易聊天室实践

    1.前言 公司游戏里面有个简单的聊天室,了解了之后才知道是node+websocket做的,想想php也来做个简单的聊天室.于是搜集各种资料看文档.找实例自己也写了个简单的聊天室. http连接分为短 ...

  7. 基于select的python聊天室程序

    python网络编程具体参考<python select网络编程详细介绍>. 在python中,select函数是一个对底层操作系统的直接访问的接口.它用来监控sockets.files和 ...

  8. 用SignalR 2.0开发客服系统[系列2:实现聊天室]

    前言 交流群:195866844 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室 ...

  9. 基于NodeJS的秘室聊天室

    借着放假期间将NodeJS重新回顾了一下并玩了一下sketch来进行设计界面,很不错.(注:代码整理后会放到github上,请关注.) 本次聊天室我给它定义了一个名称叫“秘密聊天室”. 需求: 技术选 ...

随机推荐

  1. main函数的参数的用法

    说明:main函数的参数的用法源代码: #include <stdio.h>#include <stdlib.h>int main(int argc, char *argv[] ...

  2. PKUSC2018 Slay The Spire

    有攻击牌和强化牌各 $n$ 张,强化牌可以让之后所有攻击牌攻击力乘一个大于 $1$ 的系数,攻击牌可以造成伤害 求所有“抽出 $m$ 张然后打 $k$ 张”能造成的伤害之和 $k,m,2n \leq ...

  3. UVALive - 4270 Discrete Square Roots (扩展欧几里得)

    给出一组正整数$x,n,r$,使得$r^2\equiv x(mod\: n)$,求出所有满足该等式的$r$. 假设有另一个解$r'$满足条件,则有$r^2-r'^2=kn$ 因式分解,得$(r+r') ...

  4. Python之用虚拟环境隔离项目,并重建依赖关系

    下面将以安装django和mysqlclient介绍如何用虚拟环境隔离项目,并重建依赖关系.操作系统:windows 10:python版本:python3.7 1. 安装python虚拟环境 (1) ...

  5. 【LeetCode】009. Palindrome Number

    Determine whether an integer is a palindrome. Do this without extra space. Some hints: Could negativ ...

  6. BZOJ2038:[2009国家集训队]小Z的袜子

    浅谈莫队:https://www.cnblogs.com/AKMer/p/10374756.html 题目传送门:https://lydsy.com/JudgeOnline/problem.php?i ...

  7. 日志管理系统ELK6.2.3

    https://www.jianshu.com/p/88f2cbedcc2a 写在前面 刚毕业工作的时候,处理日志喜欢自己写脚本抓取数据分析日志,然后在zabbix上展示出来.但是开发要看日志的时候, ...

  8. 蓝桥杯 算法训练 ALGO-139 s01串

    算法训练 s01串 时间限制:1.0s 内存限制:256.0MB 问题描述  s01串初始为”0”  按以下方式变换  0变1,1变01 输入格式  1个整数(0~19) 输出格式  n次变换后s01 ...

  9. 机器学习:集成学习(集成学习思想、scikit-learn 中的集成分类器)

    一.集成学习的思想 集成学习的思路:一个问题(如分类问题),让多种算法参与预测(如下图中的算法都可以解决分类问题),在多个预测结果中,选择出现最多的预测类别做为该样本的最终预测类别: 生活中的集成思维 ...

  10. Thread之三:Thread Join()的用法

    一.join用法 join()和wait()不会释放锁,join()是Thread的方法,wait()是Object的方法 1.join方法定义在Thread类中,则调用者必须是一个线程 例如: Th ...