基于TCP编写,一个聊天室C/S。
基本要求
注
暂且不含线程池、多线程编程、超时重传、确认收包等
后续待加功能
//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);
//client.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_IP);
// 创建套接字(socket)
int sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock < 0) { perror("sock error"); exit(-1); }
//向服务器发出连接请求(connect)
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connect error");
exit(-1);
}
分类 | 阻塞 | 非阻塞 |
---|---|---|
概念 | 对一个文件描述符指定的文件或设备, 一般有两种工作方式: 阻塞与非阻塞方式 | |
当试图对该文件描述符进行读写时,如果当时没有数据可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止 | 如果没有数据可读,或者不可写,读写函数马上返回,而不会等待 | |
举例 | 比如说小明去找一个女神聊天,女神却不在 | |
如果小明舍不得走,只能在女神大门口死等着,当然小明可以休息。当女神来了,她会把你唤醒(囧,因为挡着她门了),这就是阻塞方式 | 如果小明舍不得走,只能在女神大门口死等着,当然小明可以休息。当女神来了,她会把你唤醒(囧,因为挡着她门了),这就是阻塞方式 | |
区别 | 等待 | 立即返回 |
//utility.h代码(设置非阻塞函数模块):
//将文件描述符设置为非阻塞方式(利用fcntl函数)
int setnonblocking(int sockfd)
{
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
return 0;
}
epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本。其实现和使用方式与 select/poll 有很多不同,epoll 通过一组函数来完成有关任务,而不是一个函数。
epoll 之所以高效,是因为 epoll 将用户关心的文件描述符放到内核里的一个事件表中,而不是像 select/poll 每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll 无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入就绪队列的描述符集合就行了。
与select相比,epoll分清了频繁调用和不频繁调用的操作
epoll 有两种工作方式,LT(level triggered):水平触发和 ET(edge-triggered):边沿触发。LT 是 select/poll 使用的触发方式,比较低效;而 ET 是 epoll 的高速工作方式(本项目使用 epoll 的 ET 方式)。
通俗理解就是,比如说有一堆女孩,有的很漂亮,有的很凤姐。现在你想找漂亮的女孩聊天,LT 就是你需要把这一堆女孩全都看一遍,才可以找到其中的漂亮的(就绪事件);而ET是你的小弟(内核)将N个漂亮的女孩编号告诉你,你直接去看就好,所以 epoll 很高效。另外,还记得小明找女神聊天的例子吗?采用非阻塞方式,小明还需要每隔十分钟回来看一下(select);如果小明有小弟(内核)帮他守在大门口,女神回来了,小弟会主动打电话,告诉小明女神回来了,快来处理吧!这就是 epoll。
epoll 共3个函数, 如下:
int epoll_create(int size)
//创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll所支持的最大句柄数
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(表示对应的文件描述符可以读,即读事件发生),其他宏类型,可以google之!
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,函数返回需要处理的事件数目(该数目是就绪事件的数目,就是前面所说漂亮女孩的个数N)
服务端使用 epoll 的时候,步骤如下:
1、 调用 epoll_create 函数在 Linux 内核中创建一个事件表;
2、然后将文件描述符(监听套接字 listener )添加到所创建的事件表中;
3、在主循环中,调用 epoll_wait 等待返回就绪的文件描述符集合;
4、分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件( epoll 还有很多其他事件,本项目为简洁明了,不介绍)。
举例:下面介绍下如何将一个 socket 添加到内核事件表中
//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");
}
#ifndef UTILITY_H_INCLUDED
#define UTILITY_H_INCLUDED
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// clients_list save all the clients's socket
list 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);
list::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
/* 限于篇幅,这里先介绍下utility.h的主要构成 */
//服务端存储所有在线用户socket, 便于广播信息
list 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);
#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;
}
//client.cpp代码(管道模块)
// 创建管道.
int pipe_fd[2];
if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }
#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;
}
1、Linux下基于socket和多线程的聊天室小程序
2、Linux C实现简单的网络聊天室
3、C 基于UDP实现一个简易的聊天室
4、C语言利用epoll实现高并发聊天室–实验楼
5、3、UNIX网络编程学习笔记–TCP套接字编程
6、Linux下的I/O复用与epoll详解