用来练手熟悉Linux C/C++编程的小服务器,只有一个文件。由于重心在熟悉Linux相关函数上,所以写的很简陋。
开发环境:Ubuntu 18.04.1 LTS 个人的腾讯云服务器
运行方法:直接将cpp文件编译运行即可,运行时需要提供服务器ip和port
主要涉及信号、Epoll等基础知识点
最重要的是:win10的telnet是每次实时发送字符(这与ubuntu是不同的),所以我在服务器端添加了string同时如果检测到telnet发送了长度为2的"\r\n"(这是win系统中换行字符),就从对应string里拿出相应积累的字符串并将其输出到其他用户终端上。所以这个程序的客户端telnet不能是linux系统下的telnet服务。
源代码如下
//code1.cpp
//用于一对一聊天的程序
//Ubuntu 18.04.1 LTS
//客户端要求:Win10原装telnet程序即可
//win10 cmd 命令: telnet 服务器ip 服务器指定端口
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/sendfile.h>
// #include<sys/uio.h>
// #include<sys/stat.h>
// #include<sys/types.h>
#include<fcntl.h>
// #include<pthread.h>
#include<sys/epoll.h>
// #include<sys/wait.h>
// #include<sys/mman.h>
#include<signal.h>
#include<string>
using namespace std;
/* 通过缓存消息依次分发的方式进行管理 */
/* epoll进行IO复用 采用统一信号管道 */
/* 发送方采用win10 telnet单字节发送 单行消息的结束符号识别为"\r\n" */
class UserInfo{
public:
//构造函数
UserInfo():fd(-1){};
//初始化
void init(int fd){this->fd=fd;this->buffer="";};
//清除
void clear(){this->fd=-1;this->buffer="";};
//用户文件描述符
int fd;
//用户缓冲区
string buffer;
};
/* 全局变量 */
//缓存区大小
const int BUFFER_SIZE = 1024;
//epoll列表大小
const int MAX_EPOLL_NUMBER = 10;
//backlog值
const int BACKLOG = 5;
//最大接入用户数量
const int MAX_USER_NUM = 2;
//当前在线用户数量 <=2
int cur_user_num = 0;
//当前在线用户的信息
UserInfo users[MAX_USER_NUM];
//统一信号管道 所有信号 => sig_pipefd[1] ==> sig_pipefd[0]
int sig_pipefd[2];
/* 预备函数 */
//自定义信号处理函数
void handler(int sig){
int save_errno = errno;
int msg = sig;
send(sig_pipefd[1],(void*)&msg,1,0);
errno = save_errno;
}
//添加信号
void addsig(int sig,void(handler)(int)){
struct sigaction sa;
memset(&sa,0,sizeof(sa));
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
sa.sa_handler = handler;
assert( sigaction(sig,&sa,NULL) != -1 );
}
//记录客户端文件描述符 成功0 超量失败-1
int adduserfd(int fd){
//使用前最好先检查当前在线用户数量
if(cur_user_num == MAX_USER_NUM)return -1;
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd == -1){
users[i].init(fd);
cur_user_num++;
return 0;
}
}
return -1;
}
//从客户端文件描述符记录表中删除指定的fd 成功0 没有找到失败-1
int deluserfd(int fd){
//使用前最好先确保fd是有效的在线用户的文件描述符
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd == fd){
users[i].clear();
cur_user_num--;
return 0;
}
}
return -1;
}
//向fd指定的客户链接添加char*t指定的长度为len的字符串
int addCharsTofd(int fd,char* t,int len){
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd == fd){
for(int k=0;k<len;++k)users[i].buffer+=t[k];
return 0;
}
}
return -1;
}
//获取并销毁fd中目前存储的数据
int getCharsfromfd(int fd,string& Chars){
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd == fd){
Chars = users[i].buffer;
users[i].buffer = "";
return 0;
}
}
return -1;
}
//设置文件描述符非阻塞
int setnonblocking(int fd){
int old_option = fcntl(fd,F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd,F_SETFL,new_option);
return old_option;
}
//向epollfd中注册文件描述符fd(非阻塞)
void addfd(int epollfd,int fd){
epoll_event event;
bzero(&event,sizeof(event));
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
setnonblocking(fd);
}
//向fd客户端发送来自服务器的系统消息
void sendSysMsg(int fd,const char* msg){
string line = string("ServerMsg:[") + string(msg) + string("]\r\n");
int len = line.size();
char* buf = new char[len+10];
memcpy(buf,line.c_str(),line.size());
buf[line.size()] = '\0';
printf("SendSysMsgtoUserfd[%d]: %s",fd,buf);
send(fd,buf,strlen(buf),0);
delete[] buf;
}
//向在线的所有用户广播系统消息
void allSendSysMsg(const char* msg){
string line = string("ServerMsg:[") + string(msg) + string("]\r\n");
int len = line.size();
char* buf = new char[len+10];
memcpy(buf,line.c_str(),line.size());
buf[line.size()] = '\0';
int msgLenth = strlen(buf);
printf("SendSysMsgforAllUsers: %s",buf);
//向全员发送系统消息
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd != -1){
send(users[i].fd,buf,msgLenth,0);
}
}
delete[] buf;
}
//删除epollfd中对fd文件描述符的监听
void delfd(int epollfd,int fd){
epoll_event event;
bzero(&event,sizeof(event));
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&event);
}
//从fd获取消息并发送到其他在线用户中
int sendMsgFromfd(int msgfd){
string msg = "";
if(getCharsfromfd(msgfd,msg) == -1)return -1;
//构建缓冲消息
string beifen = msg;
msg += "\r\n";//补齐win版本换行符
char* buf = new char[msg.size() + 4];
memset(buf,0,sizeof(buf));
memcpy(buf,msg.c_str(),msg.size());
//分发消息
for(int k=0;k<MAX_USER_NUM;++k){
if( users[k].fd != -1 && users[k].fd != msgfd ){
send(users[k].fd,buf,strlen(buf),0);
}
}
delete[] buf;
printf("[sockfd]:%d send [MSG]:%s\n",msgfd,beifen.c_str());
return 0;
}
//核心程序
int main(int argc,char* argv[]){
if(argc <= 2){
printf("usage: %s ip_address port\n",basename(argv[0]));
return 1;
}
//初始化服务器参数
printf("Init Server System...\n");
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = -1;
sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(AF_INET,SOCK_STREAM,0);
assert(listenfd >= 0);
ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
assert(ret != -1);
ret = listen(listenfd,BACKLOG);
assert(ret != -1);
//初始化全双工管道
printf("Init Pipe...\n");
ret = socketpair(PF_UNIX,SOCK_STREAM,0,sig_pipefd);
assert(ret != -1);
setnonblocking(sig_pipefd[1]);//设置管道非阻塞
//初始化epoll内核表
printf("Init Epoll...\n");
int epollfd = epoll_create(MAX_EPOLL_NUMBER);
assert(epollfd >= 0);
//epoll内核监听注册
addfd(epollfd,listenfd);//注册epoll对listenfd的监听
addfd(epollfd,sig_pipefd[0]);//注册epoll对信号管道0端的监听
//设置信号处理
printf("Init Signals...\n");
addsig(SIGTERM,handler);
addsig(SIGINT,handler);
addsig(SIGCHLD,handler);
addsig(SIGPIPE,SIG_IGN);
//核心循环
bool m_stop = false;
epoll_event events[MAX_EPOLL_NUMBER];
printf("Server Running..\n");
while(!m_stop){
int number = epoll_wait(epollfd,events,MAX_EPOLL_NUMBER,-1);
printf("epoll number == %d\n",number);
if( (number < 0) && (errno != EINTR) ){
printf("epoll error\n");
break;
}
//循环检测事件
for(int i=0;i<number;++i){
int sockfd = events[i].data.fd;
if( (sockfd == listenfd) && (events[i].events & EPOLLIN) ){
//新的客户链接需要接入
sockaddr_in client_address;
socklen_t client_length = sizeof(client_address);
int client_fd = accept(sockfd,(struct sockaddr*)&client_address,&client_length);
if(client_fd < 0){
printf("accept error [fd %d] [errno %d]\n",client_fd,errno);
continue;
}
//注册新用户信息
ret = adduserfd(client_fd);
if(ret == -1){
//用户已满无法注册登录
const char* nolink = "Sorry Full in Chating.\r\n";
send(client_fd,nolink,strlen(nolink),0);
close(client_fd);
printf("One User Get into Link but Failed for Full\n");
continue;
}
printf("One User Get into Link\n");
//注册关于client_fd的监听
addfd(epollfd,client_fd);
//发送系统消息
sendSysMsg(client_fd,"Welcome to ChatServer Based on TELNET");
//向其他用户发送新用户消息
for(int k=0;k<MAX_USER_NUM;++k){
int aim = users[k].fd;
if(aim != -1 && aim != client_fd){
sendSysMsg(aim,"One User Get into Server");
}
}
}else if( (sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN) ){
//存在信号通知
char signals[100];
memset(signals,0,sizeof(signals));
ret = recv(sockfd,signals,100,0);
printf("Get Signals Num is %d\n",ret);
//处理信号
for(int k=0;k<ret;++k){
switch(signals[k]){
case SIGCHLD:{
printf("SIG:[SIGCHLD]\n");
break;
}
case SIGTERM:
case SIGINT:{
if(signals[k] == SIGTERM) printf("sig:[SIGTERM]\n");
else printf("sig:[SIGINT]\n");
m_stop = true;
break;
}
default:break;
}
}
}else if( events[i].events & EPOLLIN ){
//存在输入事件
char buf[BUFFER_SIZE];
memset(buf,0,sizeof(buf));
ret = recv(sockfd,buf,BUFFER_SIZE-1,0);
printf("msg recv() len == %d\n",ret);
if(ret < 0){
if(errno != EAGAIN){
//断开客户端sockfd的连接
deluserfd(sockfd);
delfd(epollfd,sockfd);
close(sockfd);
allSendSysMsg("One User Left Just Now");
printf("link break for recv()<0\n");
}
}else if(ret == 0){
//断开客户端sockfd的连接
deluserfd(sockfd);
delfd(epollfd,sockfd);
close(sockfd);
allSendSysMsg("One User Left Just Now");
printf("link break for recv()==0\n");
}else{//主业务逻辑
if(ret == 2 && buf[0] == '\r' && buf[1] == '\n'){
//向其他用户发送消息
sendMsgFromfd(sockfd);
}else{
//添加字符串到缓存
addCharsTofd(sockfd,buf,ret);
}
}
}else{
continue;
}
}//for
}//while
//关闭所有仍在线的客户端链接
for(int i=0;i<MAX_USER_NUM;++i){
if(users[i].fd != -1){
int fd = users[i].fd;
sendSysMsg(fd,"Chat Server's System Exit");
users[i].clear();
delfd(epollfd,fd);
close(fd);
}
}
close(listenfd);
close(epollfd);
close(sig_pipefd[0]);
close(sig_pipefd[1]);
printf("System Exit\n");
return 0;
}
// g++ code1.cpp -o code1
// ./code1 172.17.0.6 12345
// g++ code1.cpp -o code1 && ./code1 172.17.0.6 12345
头文件有冗余,可以筛选一下,已经注释掉了一部分没用的头文件。注意Linux与Win在换行上的差异。