概述
使用了 tcp
+ epoll
,实现网络聊天室
1.0 版,用户的显示框和输入框在一起
2.0 版,用户的显示框与输入框分离
功能
主要功能:用户连接服务器,就会自动进入网络聊天室与其他在线用户一起聊天
服务器搭建
-
创建用户数组
- 根据配置文件中的最大用户数量,创建用户数组,并初始化(用户名字、是否在线、通信套接字)
- 创建一个用户记录器,记录当前在线用户数量
-
建立一个
tcp
类型的正在监听的套接字 -
使用
epoll
管理所有套接字,监听所有用户的连接申请、发送消息和退出-
有新的用户连接
-
如果用户数组已满,跳过
-
如果有位置
- 把新用户放在这(设为在线,记录名字,记录用户套接字)
- 用户记录器++
- 将新用户加入
epoll
- 给新用户发送欢迎信息
- 通知其他在线用户,新用户的到来
-
-
有用户发信息
- 接收
- 定位是哪个用户发消息
- 如果用户断开
- 合成用户退出信息
- 将此用户下线
- 用户记录器 - -
- 移除
epoll
监听 - 通知其他所有在线用户
- 如果是正常消息,转发给其他所有在线用户
- 如果用户断开
-
客户端搭建
- 连接聊天室服务器
- 输入自己的名字
- 进入聊天室聊天
- 服务器套接字就绪,接收信息并输出在屏幕上
- 标准输入就绪,接收信息发送给服务器
启动
启动服务器
1、在bin
目录下生成可执行文件
w@Ubuntu20:bin $ gcc ../src/*.c -o server
2、启动服务器
w@Ubuntu20:bin $ ./server ../conf/server.conf
启动客户端
1、在客户端的目录下生成可执行文件
w@Ubuntu20:client $ gcc *.c -o client
2、启动客户端
w@Ubuntu20:client $ ./client client.conf
目录设计
服务器
- bin:存放二进制文件
- conf:存放配置文件
- include:存放头文件
- src:存放源文件
w@Ubuntu20:src $ tree ..
..
├── bin
│ └── server
├── conf
│ └── server.conf
├── include
│ └── qq.h
└── src
├── interact.c
├── main_server.c
└── tcp_init.c
客户端
w@Ubuntu20:client $ tree
.
├── client
├── client.conf
├── interact_qq.c
└── main_client.c
配置文件
服务器配置文件 server.conf
存放服务器ip
地址,服务器port
端口,聊天室最大用户数量
根据实际情况自行更改
192.168.160.129
2000
10
客户端配置文件 client.conf
存放服务器ip
地址,服务器port
端口
根据实际情况自行更改
192.168.160.129
2000
代码
服务器代码
qq.h
#ifndef __QQ_H__
#define __QQ_H__
//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
perror(msg);\
return -1;} }
//用户信息
typedef struct {
char _online;//是否在线
int _usrfd;//用户套接字
char _usrname[30];//用户名
}Usr_t, *pUsr_t;
//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port);
//功能:服务器主进程处理来自用户的连接,发信息,退出
//参数:服务器套接字,用户数组,用户最大容量
int interact_usr(int sfd, pUsr_t pUsrArr, int max_capacity);
#endif
main_server.c
#include "../include/qq.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//检查命令行参数个数
#define ARGS_CHECK(argc, num) { if (argc != num) {\
fprintf(stderr, "Args error!\n");\
return -1; }}
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 2);
//从配置文件中取出服务器ip、port、最多的在线用户数
FILE *fp = fopen(argv[1], "r");
char ip[128] = {0};
int port = 0;
int max_capacity = 0;
fscanf(fp, "%s%d%d", ip, &port, &max_capacity);
fclose(fp);
//创建用户数组,存储用户信息
pUsr_t pUsrArr = (pUsr_t)calloc(max_capacity, sizeof(Usr_t));
//建立一个正在监听的tcp类型的服务器套接字
int sfd = tcp_init(ip, port);
printf("qq_server boot...\n");
printf("max_capacity : %d\n", max_capacity);
//处理用户端请求
interact_usr(sfd, pUsrArr, max_capacity);
//关闭服务器套接字
close(sfd);
printf("qq_server was closed!\n");
//释放用户数组
free(pUsrArr);
pUsrArr = NULL;
return 0;
}
tcp_init.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
perror("msg"); return -1;} }
//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port)
{
//生成一个tcp类型的套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "ser_socket");
//将端口号设置为可重用, 不用再等待重启时的TIME_WAIT时间
int reuse = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
//给套接字绑定服务端ip和port
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(struct sockaddr_in));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(ip);
serverAddr.sin_port = htons(port);
int ret = bind(sfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
ERROR_CHECK(ret, -1, "ser_bind");
//将套接字设为监听模式,并指定最大监听数(全连接队列的大小)
ret = listen(sfd, 10);
ERROR_CHECK(ret, -1, "ser_listen");
/* printf("[ip:%s, port:%d] is listening...\n", ip, port); */
return sfd;
}
interact_usr.c
#include "../include/qq.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
//将fd加入epfd
int epollAddFd(int fd, int epfd)
{
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = EPOLLIN;
event.data.fd = fd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
ERROR_CHECK(ret, -1, "EPOLL_CTL_ADD");
return 0;
}
//将fd从epfd中移除
int epollDelFd(int fd, int epfd)
{
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = EPOLLIN;
event.data.fd = fd;
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
ERROR_CHECK(ret, -1, "EPOLL_CTL_DEL");
return 0;
}
//用fd查找用户
pUsr_t search_usr(pUsr_t pArr, int len, int fd)
{
int i;
for (i = 0; i < len; ++i) {
if (fd == pArr[i]._usrfd) {
return pArr + i;
}
}
return NULL;
}
//功能:服务器主进程处理来自用户的连接,发信息,退出
//参数:服务器套接字,用户数组,用户最大容量
int interact_usr(int sfd, pUsr_t pUsrArr, int max_capacity)
{
//使用epoll管理所有文件描述符
int epfd = epoll_create(1);
//将sfd添加进epfd
epollAddFd(sfd, epfd);
int ret = -1;
char buf[256] = {0};//读写缓冲区
int readyFdNum = 0;//就绪的文件描述符数量
struct epoll_event evs[2]; //epoll_wait等待数组的大小
int newfd = 0;//客户端的套接字
int cur_count = 0; //当前在线用户数量
//epoll等待就绪的文件描述符
while (1) {
readyFdNum = epoll_wait(epfd, evs, 2, -1);
int i;
for (i = 0; i < readyFdNum; ++i) {
//服务端套接字就绪,有新用户连接
if (evs[i].data.fd == sfd) {
//接收用户端
newfd = accept(sfd, NULL, NULL);
//如果用户数组已满,不再接收新用户的连接
if (cur_count == max_capacity) {
strcpy(buf, "网络聊天室人数已满,无法加入");
send(newfd, buf, strlen(buf), 0);
close(newfd);
continue;
}
//接收用户名
memset(buf, 0, sizeof(buf));
recv(newfd, buf, sizeof(buf) - 1, 0);
//放入用户数组
int j;
for (j = 0; j < max_capacity; ++j) {
if (0 == pUsrArr[j]._online) {
pUsrArr[j]._usrfd = newfd;//记录用户通信fd
pUsrArr[j]._online = 1;//新用户状态更新成在线
strcpy(pUsrArr[j]._usrname, buf);//设置用户名
//将新用户加入epoll监听
epollAddFd(pUsrArr[j]._usrfd, epfd);
//给新用户发送欢迎信息
strcpy(buf, "===========================Welcome to the best Online Chat Romm==========================\n\n");
send(newfd, buf, strlen(buf), 0);
//通知其他在线用户,有新用户加入
memset(buf, 0, sizeof(buf));
sprintf(buf, "==[new user %s join!]==", pUsrArr[j]._usrname);
for (int k = 0; k < max_capacity; ++k) {
if (pUsrArr[k]._online && pUsrArr[k]._usrfd != pUsrArr[j]._usrfd) {
send(pUsrArr[k]._usrfd, buf, strlen(buf), 0);
}
}
break;
}
}
++cur_count;//用户记录器++
printf("cur_count : %d\n", cur_count);
}
//用户端发来消息
else {
//接收消息
memset(buf, 0, sizeof(buf));
ret = recv(evs[i].data.fd, buf, sizeof(buf) - 1, 0);
//定位发消息的用户
pUsr_t pCur = search_usr(pUsrArr, max_capacity, evs[i].data.fd);
//如果用户退出
if (0 == ret) {
--cur_count;//用户记录器--
pCur->_online = 0;//用户下线
sprintf(buf, "==[usr_%s exit!]==",pCur->_usrname);
puts(buf);
printf("cur_count : %d\n", cur_count);
//从epoll管理的红黑树中删除
epollDelFd(pCur->_usrfd, epfd);
//通知其他用户
for (int j = 0; j < max_capacity; ++j) {
if (pUsrArr[j]._online && pCur->_usrfd != pUsrArr[j]._usrfd) {
send(pUsrArr[j]._usrfd, buf, strlen(buf), 0);
}
}
}
//正常消息,进行转发给其他在线用户
else {
char usr_info[1024] = {0};
sprintf(usr_info, "[%s]: %s", pCur->_usrname, buf);
for (int j = 0; j < max_capacity; ++j) {
if (pUsrArr[j]._usrfd != pCur->_usrfd && pUsrArr[j]._online) {
send(pUsrArr[j]._usrfd, usr_info, strlen(usr_info), 0);
}
}
}
}
}
}
return 0;
}
客户端代码
main_client.c
#include "../include/qq.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#define ARGS_CHECK(argc, num) { if (argc != num) {\
fprintf(stderr, "Args error!\n");\
return -1;} }
//与聊天室服务器交互
int interact_qq(int sfd);
int main(int argc, char *argv[])
{
ARGS_CHECK(argc, 2);
//从配置文件中,取出服务器的ip和port
FILE *fp = fopen(argv[1], "r");
char ip[128] = {0};
int port = 0;
fscanf(fp, "%s%d", ip, &port);
fclose(fp);
//连接聊天室服务器
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(ip);
serAddr.sin_port = htons(port);
int ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "connect");
//发送用户名
char username[30] = {0};
printf("准备进入聊天室,请输入用户名:");
scanf("%s", username);
send(sfd, username, strlen(username), 0);
//清空界面
system("clear");
//与聊天室交互
interact_qq(sfd);
//关闭服务器套接字
close(sfd);
return 0;
}
interact_qq.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
perror(msg);\
return -1;} }
//与聊天室服务器交互
int interact_qq(int sfd)
{
//定义一个读操作集合
fd_set rdset;
FD_ZERO(&rdset);
char buf[128] = {0};//读写缓冲区
int ret = -1;
while (1) {
//每次select前,重置读集合,因为select会修改读集合(将未就绪的文件描述符置为0)
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sfd, &rdset);
//select阻塞在此,等待集合中任意一个文件描述符就绪后,解除阻塞
//select接触阻塞后,找就绪的文件描述符,需要遍历集合去找
ret = select(sfd + 1, &rdset, NULL, NULL, NULL);
ERROR_CHECK(ret, -1, "select");
//服务端套接字就绪,表示服务端有数据到来,接收并打印在终端
if (FD_ISSET(sfd, &rdset)) {
memset(buf, 0, sizeof(buf));
ret = recv(sfd, buf, sizeof(buf) - 1, 0);
if (0 == ret) {
//服务端已关闭
printf("server exit\n");
return -1;
}
printf("%s\n", buf);
}
//标准输入就绪,接收并发送给服务端
if (FD_ISSET(STDIN_FILENO, &rdset)) {
memset(buf, 0, sizeof(buf));
ret = read(STDIN_FILENO, buf, sizeof(buf) - 1);
send(sfd, buf, strlen(buf) - 1, 0);
}
}
}
演示
总结
一个练习tcp
,epoll
的小项目