一:项目内容
本项目使用C++实现一个具备服务器端和顾客端即时通讯且具有私聊功能的聊天室。
目的是学习C++网路开发的基本概念,同时也可以熟悉下Linux下的C++程序编译和简单MakeFile编撰
二:需求剖析
这个聊天室主要有两个程序:
1.服务端:能否接受新的顾客联接,并将每位顾客端发来的信息,广播给对应的目标顾客端。
2.顾客端:能否联接服务器,并向服务器发送消息,同时可以接收服务器发来的消息。
即最简单的C/S模型。
三:具象与细化
服务端类须要支持:
1.支持多个顾客端接入,实现聊天室基本功能。
2.启动服务,构建窃听端口等待顾客端联接。
3.使用epoll机制实现并发,降低效率。
4.顾客端联接时,发送欢迎消息,并储存联接记录。
5.顾客端发送消息时,按照消息类型,广播给所有用户(群聊)或则指定用户(私聊)。
6.顾客端恳求退出时,对相应联接信息进行清除。
顾客端类须要支持:
1.联接服务器。
2.支持用户输入消息,发送给服务端。
3.接受并显示服务端发来的消息。
4.退出联接。
涉及两个事情,一个写,一个读。所以顾客端须要两个进程分别支持以下功能。
子进程:
1.等待用户输入信息。
2.将聊天信息写入管线(pipe),并发献给父进程。
父进程:
1.使用epoll机制接受服务端发来的消息,并显示给用户,使用户看见其他用户的信息。
2.将子进程发送的聊天信息从管线(pipe)中读取下来,并发献给顾客端。
四:C/S模型
TCP服务端通讯常规步骤:
1.socket()创建TCP套接字
2.bind()将创建的套接字绑定到一个本地地址和端口上
3.listen(),将套接字设为窃听模式,打算接受顾客恳求
4.accept()等用户恳求到来时接受,返回一个对应此联接新套接字
5.用accept()返回的套接字和顾客端进行通讯,recv()/send()接受/发送信息。
6.返回,等待另一个顾客恳求。
7.关掉套接字
TCP顾客端通讯常规步骤:
1.socket()创建TCP套接字。
2.connect()构建抵达服务器的联接。
3.与顾客端进行通讯,recv()/send()接受/发送信息,write()/read()子进程写入管线,父进程从管线中读取信息之后send给顾客端
5.close()关掉顾客联接。
五:相关技术介绍
1.socket阻塞与非阻塞
阻塞与非阻塞关注的是程序在等待调用结果时(消息,返回值)的状态。
阻塞调用是指在调用结果返回前,当前线程会被挂起,调用线程只有在得到调用结果以后才能返回。
非阻塞调用是指在不能立即得到结果之前,该调用不会阻塞当前线程。
eg.你打电话问书城老总有没有《网络编程》这本书,老总去书柜上找,倘若是阻塞式调用,你都会把自己仍然挂起,守在电话边上,直至得到这本书有或则没有的答案。假如是非阻塞式调用,你可以干别的事情去,隔一段时间来看一下老总有没有告诉你结果。
同步异步是对书城老总而言(同步老总不会提醒你找到结果了,异步老总会打电话告诉你),阻塞和非阻塞是对你而言。
socket()函数创建套接字时,默认的套接字都是阻塞的,非阻塞设置方法代码:
//将文件描述符设置为非阻塞形式(借助fcntl函数)
fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFD,0)|O_NONBLOCK);
2.epoll
当服务端的人数越来越多,会造成资源吃紧,I/O效率越来越低,这时就应当考虑epoll,epoll是Linux内核为处理大量句柄而改进的poll,是linux特有的I/O函数。其特征如下:
1)epoll是Linux下多路复用IO插口select/poll的提高版本linux论坛,其实现和使用方法与select/poll大有不同linux软件下载,epoll通过一组函数来完成有关任务,而不是一个函数。
2)epoll之所以高效,是由于epoll将用户关心的文件描述符放在内核里的一个风波列表中,而不是像select/poll每次调用都须要重复传入文件描述符集或风波集(大量拷贝开支),例如一个风波发生,epoll无需遍历整个被窃听的描述符集,而只须要遍历什么被内核IO风波异步唤起而加入就绪队列的描述符集合即可。
3)epoll有两种工作方法,LT(Leveltriggered)水平触发、ET(Edgetriggered)边缘触发。LT是select/poll的工作方法,比较低效,而ET是epoll具有的高速工作方法。
Epoll用法(三步曲):
第一步:intepoll_create(intsize)系统调用linux管道通信c 编程,创建一个epoll句柄,参数size拿来告诉内核窃听的数量,size为epoll支持的最大句柄数。
第二步:intepoll_ctl(intepfd,intop,intfd,structepoll_event*event)风波注册函数
参数epfd为epoll的句柄。参数op表示动作三个宏来表示:EPOLL_CTL_ADD注册新fd到epfd、EPOLL_CTL_MOD更改早已注册的fd的窃听风波、EPOLL_CTL_DEL从epfd句柄中删掉fd。参数fd为须要窃听的标示符。参数结构体epoll_event告诉内核须要窃听的风波。
第三步:intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout)等待风波的形成,通过调用搜集在epoll监控中已然发生的风波。参数structepoll_event是风波队列把就绪的风波放进去。
eg.服务端使用epoll的时侯步骤如下:
1.调用epoll_create()在linux内核中创建一个风波表。
2.之后将文件描述符(窃听套接字listener)添加到风波表中
3.在主循环中,调用epoll_wait()等待返回就绪的文件描述符集合。
4.分别处理就绪的风波集合,本项目中一共有两类风波:新用户联接风波和用户发来消息风波。
六:代码结构
每位文件的作用:
1.Common.h:公共头文件,包括所有须要的宏以及socket网路编程头文件,以及消息结构体(拿来表示消息类别等)
2.Client.hClient.cpp:顾客端类的实现
3.Server.hServer.cpp:服务端类的实现
4.ClientMain.cppServerMain.cpp顾客端及服务端的主函数。
七:代码实现
Common.h
定义一些共用的宏定义,包括一些共用的网路编程相关头文件。
1)定义一个函数将文件描述符fd添加到epfd表示的内核风波表中供顾客端和服务端两个类使用。
2)定义一个信息数据结构,拿来表示传送的信息linux管道通信c 编程,结构体包括发送方fd,接收方fd,拿来表示消息类别的type,还有文字信息。
函数recv()send()write()read()参数传递是字符串,所以在传送前/接受后要把结构体转换为字符串/字符串转换为结构体。
#ifndefCHATROOM_COMMON_H
#defineCHATROOM_COMMON_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//默认服务器端IP地址
#defineSERVER_IP”127.0.0.1″
//服务器端标语
#defineSERVER_PORT8888
//intepoll_create(intsize)中的size
//为epoll支持的最大句柄数
#defineEPOLL_SIZE5000
//缓冲区大小65535
#defineBUF_SIZE0xFFFF
//新用户登陆后的欢迎信息
#defineSERVER_WELCOME”Welcomeyoujointothechatroom!YourchatIDis:Client#%d”
//其他用户收到消息的前缀
#defineSERVER_MESSAGE”ClientID%dsay>>%s”
#defineSERVER_PRIVATE_MESSAGE”Client%dsaytoyouprivately>>%s”
#defineSERVER_PRIVATE_ERROR_MESSAGE”Client%disnotinthechatroomyet~”
//退出系统
#defineEXIT”EXIT”
//提醒你是聊天室中惟一的顾客
#defineCAUTION”Thereisonlyoneintthecharroom!”
//注册新的fd到epollfd中
//参数enable_et表示是否启用ET模式,倘若为True则启用,否则使用LT模式
staticvoidaddfd(intepollfd,intfd,boolenable_et)
structepoll_eventev;
ev.data.fd=fd;
ev.events=EPOLLIN;
if(enable_et)
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
//设置socket为非阻塞模式
//套接字立即返回,不管I/O是否完成,该函数所在的线程会继续运行
//eg.在recv(fd…)时,该函数立即返回,在返回时,内核数据还没打算好会返回WSAEWOULDBLOCK错误代码
fcntl(fd,F_SETFL,fcntl(fd,F_GETFD,0)|O_NONBLOCK);
printf(“fdaddedtoepoll!nn”);
//定义信息结构,在服务端和顾客端之间传送
structMsg
inttype;
intfromID;
inttoID;
charcontent[BUF_SIZE];
};
#endif//CHATROOM_COMMON_H
服务端类Server.hServer.cpp
服务端须要的插口:
1)init()初始化
2)Start()启动服务
3)Close()关掉服务
4)广播消息给所有顾客端函数SendBroadcastMessage()
服务端的主循环中每次就会检测并处理EPOLL中的就绪风波,而就绪风波列表主要是两种类型:新联接或新消息。服务器会依次从就绪风波列表里提取风波进行处理,若果是新联接则accept()之后addfd(),假如是新消息则SendBroadcastMessage()实现聊天功能。
Server.h
#ifndefCHATROOM_SERVER_H
#defineCHATROOM_SERVER_H
#include
#include”Common.h”
usingnamespacestd;
//服务端类,拿来处理顾客端恳求
classServer{
public:
//无参数构造函数
Server();
//初始化服务器端设置
voidInit();
//关掉服务
voidClose();
//启动服务端
voidStart();
private:
//广播消息给所有顾客端
intSendBroadcastMessage(intclientfd);
//服务器端serverAddr信息
structsockaddr_inserverAddr;
//创建窃听的socket
intlistener;
//epoll_create创建后的返回值
intepfd;
//顾客端列表
listclients_list;
};
//Server.cpp
#include
#include”Server.h”
usingnamespacestd;
//服务端类成员函数
//服务端类构造函数
Server::Server(){
//初始化服务器地址和端口
serverAddr.sin_family=PF_INET;
serverAddr.sin_port=htons(SERVER_PORT);
serverAddr.sin_addr.s_addr=inet_addr(SERVER_IP);
//初始化socket
listener=0;
//epoolfd
epfd=0;
//初始化服务端并启动窃听
voidServer::Init(){
cout