简易命令行聊天室程序

author author     2022-09-21     502

关键词:

最近学习完网络编程,决定写一个简单的聊天服务器。主要用到的技术是socket,I/O复用(epoll),非阻塞IO,进程等知识。下面主要叙述其中的关键技术点以及编写过程中遇到的问题。

0、该程序实现的基本功能

编写了一个简单的聊天室程序,该聊天室程序能够让所有的用户同时在线群聊,它分为服务器和客户端两个部分。

  • 服务器:接收客户端数据,并将该客户端数据发送给其他登录到该服务器上的客户端。
  • 客户端:从标准输入读入数据,并将数据发送给服务器,同时接收服务器发送的数据。

1、服务器端IO模型。

采用IO复用+非阻塞IO的模型,IO复用采用Linux下的epoll机制。下面介绍epoll具体的函数。

//实现epoll服务器端需要三个函数。
//1)epoll_create:创建保持epoll文件描述符的空间,即epoll例程,size只是建议的例程大小。
#include<sys/epoll.h>
int epoll_create(int size);//成功时返回epoll文件描述符,失败时返回-1
/**
2)epoll_ctl:向空间注册并且注销文件描述符。
要使用epoll_event结构体:
struct epoll_event{
    __uint32_t events;
    epoll_data_t data;
}
    typedef union epoll_data{
         void * ptr;
         int fd;
         __uint32_t u32;
         __uint64_t u64;
    }epoll_data_t;
这里注意要声明足够大的epoll_event结构体数组后,传递给epoll_eait函数时,发生变化的文件描述符信息被填入该数组。可以直接申明也可以动态分配。
op有三个宏选项:
@EPOLL_CTL_ADD:将文件描述符注册到epoll例程。
@EPOLL_CTL_DEL:从epoll例程中删除文件描述符。
@EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
events常用的可以保存的常量以及事件类型。
@EPOLLIN:需要读取数据的情况.
@EPOLLET:以边缘触发的方式得到事件通知。
@EPOLLONESHOT:发生一次事件后,相应文件描述符不在接收事件通知,需要再次设置事件才能继续使用。

**/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//成功时返回0,失败时返回-1
int epoll_wait(int wpfd,struct epoll_event* events,int maxevents,int timeout);
//成功时返回发生事件的文件描述符数,失败时返回-1

1.1 为什么IO复用需要搭配非阻塞IO?(select/epoll返回可读后还用非阻塞是不是没有意义?)

 问题分析:a、输入过程通常分为两个阶段1)等待数据从网络中到达,它被复制到内核中的某个缓冲区。2)从内核向进程复制数据。

阻塞IO模型和非阻塞IO模型如下:

技术分享技术分享

b、文件描述符就绪条件有可读,可写或者出现异常。设置非阻塞的方法有两种一种是使用fcntl函数,另一种是通过socket API创建非阻塞的socket。

int fd_sock = socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0);

 

答:select/epoll返回了可读,并不代表一定能够读取数据,因为在返回可读到调用read函数之间,是有时间间隙的,这段时间内核可能将数据丢失。也有可以多个线程同时监听该套接字,数据也可能被其他线程读取。

可以参考知乎这个问题  https://www.zhihu.com/question/37271342

1.2、epoll的条件触发LT和边缘触发ET区别。

答:条件触发方式中,只要输出缓冲中有数据就会一直注册该事件(这次没处理该事件,下次调用epoll_wait还会继续通告该事件)。

边缘触发中输入缓冲收到数据时仅注册一次事件。

边缘触发中,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据,因此需要验证输入缓冲是否为空。read函数返回-1,变量errno中的值为EAGAIN时,说明没有数据可以读。

边缘触发方式下,为什么要将套接字变为非阻塞模式呢?以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿,没有数据可读,就会一直阻塞进程,所以一定要采用非阻塞的IO函数。

边缘触发的优点是:可以分离接收数据和处理数据的时间点。

1.3、select和epoll的区别

答:select缺点:

1)针对所有文件描述符的循环语句;

2)每次都需要向操作系统传递监视对象信息。

最耗时间的是第二点向操作系统传递监视对象信息。

epoll支持ET模式,而select只支持LT模式。select的优点是:

1)服务器端接入者少的时候适用;

2)兼容性好。

1.4、服务器端发生地址分配错误(提前终止服务器端,重启的时候出现bind() error)

答:原因是先断开的主机需要进过time-wait状态,套接字进过四次挥手最后要发送ACK(A->B),最后B接收到ACK才会正常关闭,如果没有收到,会超时重传。这个时候相应的端口处于正在使用的状态,所以bind()重新分配相同的IP和port就会出错。

关闭方法:在套接字可选项中更改SO_REUSEADDR状态,将0改为1即可。(客户端是调用connect随机分配IP&port,所以不会出现该错误)

2、客户端client

client采用分割读写的方法进行操作,子进程负责发送数据,父进程负责接收数据。

分离流的好处:

1)减低实现难度;

2)与输入无关的输出操作可以提高速度。

    pid_t pid = fork();
    if(pid == 0){//子进程负责写
        write_routine(clntSock,buff);
    }
    else{//父进程负责读
        read_routine(clntSock,buff);
    }

 

3、具体实现代码。

//utility.h
#ifndef _UTILITY_
#define _UTILITY_

#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<list>
#include<string>

using namespace std;
/*存储客户端文件描述符*/
list<int> clientLists;

#define MAX_EVENT_NUMBER 1024

#define BUFF_SIZE 400

/*服务器ip*/
#define SERVERIP "127.0.0.1"

/*端口号(只要在1024~5000都行)*/
#define PORT "6666"

/*epoll例程大小*/
#define EPOLLSIZE 50 

#define EXIT "exit"

/**
  *将文件描述符设置成非阻塞的
  *返回文件描述符旧的状态,以便日后恢复该状态标志
**/
int setNonBlocking(int fd){
    int oldOption = fcntl(fd,F_GETFL);
    int newOption = oldOption | O_NONBLOCK;
    fcntl(fd,F_SETFL,newOption);
    return oldOption;
}

/**
 * 将文件描述符fd上的EPOLLIN注册到epollfd指示的内核事件表中
 * 参数enable_et指定是否对fd启用ET模式  
**/
void addfd(int epollfd,int fd,bool enable_et){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;//主要读取客服端套接字信息
    if(enable_et){
        event.events |= EPOLLET;
    }
    setNonBlocking(fd);
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);    
}
/**
 * 服务端向其他客户端发送消息
 **/
void sendBroadCast(struct epoll_event* waitEvents,int eventsNumber,int epollfd,int listenfd){
    int clntSock = 0;
    struct sockaddr_in clntAdr;
    char buff[BUFF_SIZE];
    for(int i = 0;i < eventsNumber;++i){
        if(waitEvents[i].data.fd == listenfd){//未建立连接,先建立连接
        socklen_t clientLength = sizeof(clntAdr);
            clntSock = accept(listenfd,(struct sockaddr*) &clntAdr,&clientLength);
            addfd(epollfd,clntSock,true);
            /*第一次connect*/
            const char* message = "welcome join chatting!\n\n";
            printf("%d join chatting!!!\n",clntSock);
            write(clntSock,message,strlen(message));
            /*将新clientID加入链表*/
            clientLists.push_back(clntSock);
            /*向例程中注册事件*/
            addfd(epollfd,clntSock,true);
        }
        else{//已经建立连接,需要读取数据,然后发送给其他客户端
            clntSock = waitEvents[i].data.fd;
            bzero(&buff,strlen(buff));
            int strLen = sprintf(buff,"te clientID %d saying: ",clntSock);
            strLen += read(clntSock,buff + strLen,BUFF_SIZE);
            if(strLen < 0){//客户端读取数据出错
                perror("read");
                close(clntSock);
                exit(-1);
            }
            else if(strLen == 0){//已经没数据,需要关闭客户端
                epoll_ctl(epollfd,EPOLL_CTL_DEL,clntSock,NULL);
                clientLists.remove(clntSock);
                close(clntSock);                    
            }
            else{
                buff[strLen] = 0;
                /*发送给其他的所有客户端*/ 
                if(clientLists.size() == 1){
                    const char *mess = "Atention!only one client in the chatting room!\n";
                    write(clntSock,mess,strlen(mess));
                    printf("Atention!only ID %d client in the chatting room!\n",clntSock);                    
                }                
                
                printf("saved: %s\n",buff);

                list<int> :: iterator iter;
                for(iter = clientLists.begin();iter != clientLists.end();++iter){
                    if(*iter == clntSock){
                        continue;
                    }                
                    write(*iter,buff,strLen + 1);

                }
            }
        }
    }        
}

#endif
//server.cpp
#include"utility.h"

int main(){
    int err = 0;
    char buff[BUFF_SIZE];
    struct sockaddr_in servAddr;
    bzero(&servAddr,sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    inet_aton(SERVERIP,&servAddr.sin_addr);//将字符串IP地址转化为32位整数型数据
    servAddr.sin_port = htons(atoi(PORT));
    

    /*监听套接字描述符*/
    int listenfd = socket(PF_INET,SOCK_STREAM,0);
    if(listenfd == -1){
        perror("listenfd");
        exit(1);
    }
    /*更改服务器套接字的time_wait状态*/
    int option = 0;
    socklen_t optlen;
    optlen = sizeof(option);
    option = 1;
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,optlen);

    /*分配IP地址和端口号*/
    err = bind(listenfd,(struct sockaddr*)&servAddr,sizeof(servAddr));
    if(err == -1){
        perror("bind");
        exit(1);
    }
    /*转化为可接受请求转态*/
    err = listen(listenfd,10);
    if(err == -1){
        perror("listen");
        exit(1);
    }

    int epfd = epoll_create(EPOLLSIZE);    
    struct epoll_event waitEvents[MAX_EVENT_NUMBER]; //预留足够大的空间来存储后面发生变化的事件,也可以使用动态分配  
    
    /*注册监听套接字*/
    addfd(epfd,listenfd,true);    

    /*监测文件描述符的变化*/
    int eventsNumber = 0;    
    while(1){  
        eventsNumber = epoll_wait(epfd,waitEvents,EPOLLSIZE,-1);//一直等待事件的发生,除非出错返回
        if(eventsNumber == -1){
            perror("eventsNumber");
            exit(1);
        }        
        sendBroadCast(waitEvents,eventsNumber,epfd,listenfd);//将waitEvents当作平常的数组,数组名就是指针
    }
    close(listenfd);
    close(epfd);
    return 0;
}
#include"utility.h"

void read_routine(int clntSock,char *buf);
void write_routine(int clntSock,char *buf);

int main(){
    int clntSock;
    char buff[BUFF_SIZE];
    clntSock = socket(PF_INET,SOCK_STREAM,0);

    if(clntSock == -1){
        perror("clntSock");
        exit(1);
    }
    struct sockaddr_in servAdr;
    bzero(&servAdr,sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    inet_aton(SERVERIP,&servAdr.sin_addr);//将字符串IP地址转化为32位整数型数据
    servAdr.sin_port = htons(atoi(PORT));

    int err = connect(clntSock,(struct sockaddr*)&servAdr,sizeof(servAdr));
    if(err == -1){
        perror("connect");
        exit(1);
    }

    pid_t pid = fork();
    if(pid == 0){//子进程负责写
        write_routine(clntSock,buff);
    }
    else{//父进程负责读
        read_routine(clntSock,buff);
    }
    close(clntSock);
    return 0;
}

void read_routine(int clntSock,char *buf){
    while(1){
        int strLen = read(clntSock,buf,BUFF_SIZE);
        if(strLen == 0){
            return;
        }
        buf[strLen] = 0;
        printf("%s",buf);
    }
}

void write_routine(int clntSock,char *buf){
    while(1){
        fgets(buf,BUFF_SIZE,stdin);
        if(!strcmp(buf,"exit\n")){
            shutdown(clntSock,SHUT_WR);
            return;
        }
        write(clntSock,buf,strlen(buf));
    }
}

 

命令行显示的简易进度条

测试。。。 进度条类:packagecom.test;publicclassProcessBar/***显示一个进度条*/privatestaticintcount=1;privatestaticbooleanisStart=false;publicstaticvoidprocessbarshow(intnum,inttotal)/***总共显示30个*_________________ 查看详情

laravel+swoole打造im简易聊天室(代码片段)

Laravel+Swoole打造IM简易聊天室应用场景:实现简单的即时消息聊天室(一)扩展安装(二)webSocket服务端代码(三)客户端实现应用场景:实现简单的即时消息聊天室(一)扩展安装peclinstallswoole安装完成后可以通过以下命令检测Sw... 查看详情

laravel+swoole打造im简易聊天室(代码片段)

Laravel+Swoole打造IM简易聊天室应用场景:实现简单的即时消息聊天室(一)扩展安装(二)webSocket服务端代码(三)客户端实现应用场景:实现简单的即时消息聊天室(一)扩展安装peclinstallswoole安装完成后可以通过以下命令检测Sw... 查看详情

使用socket在cmd命令行聊天

---恢复内容开始---Socket:两个程序通过一个双向的通信连接实现数据的交换,这个链接的一端就称为Socket特性:持久链接,双向通信首先要有服务器与客户端两端开一个服务器server,引用node的核心模块netconstnet=require("net");constclients... 查看详情

linux实现简易的shell命令行解释器(代码片段)

...c;进程等待,进程替换,那么通过这些来制作一个简易的Shell命令行解释器。首先这是与Shell的互动:用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右... 查看详情

golang一个简单的命令行聊天室,如irc(代码片段)

查看详情

linuxc语言のudp简易聊天室sokcet(代码片段)

设计思路考虑到只是一个简易版本的UDP聊天服务,所以很多不完善的地方服务器服务器我是开了一个父子进程,分别负责的接受客户端的消息&&发送某一个客户端的信息服务器的命令终端(只不过没有实现,只写... 查看详情

golang命令行库cobra的使用(代码片段)

...件的程序。下面是Cobra使用的一个演示:Cobra提供的功能简易的子命令行模式,如appserver,appfetch等等完全兼容posix命令行模式嵌套子命令subcommand支持全局,局部,串联flags使用Cobra很容易的生成应用程序和命令,使用cobracreateappnam... 查看详情

从命令行进行 Lync 聊天

】从命令行进行Lync聊天【英文标题】:LyncChatFromCommandLine【发布时间】:2018-01-1922:57:13【问题描述】:我正在尝试从命令行启动LyncIM会话。我让Lync运行。如果我使用:"C:\\ProgramFiles\\MicrosoftOffice\\root\\Office16\\lync.exe"sip:user@address.co... 查看详情

通过java中的jar命令在命令行中生成可执行的jar文件

...等工作。就拿我大二疫情期间在家闲暇时间写的一个仿QQ聊天室来举例吧 首先将我们编写好的源代码放在一个目录的文件夹下然后将上面的所有源码复制到IDEA中,通过IDEA集成开发环境去生成对应的字节码文件,最后复制粘... 查看详情

nio代码实现简易多人聊天(代码片段)

这几天在学习nio相关知识。实现了一个简单的多人聊天程序。服务端代码;1importjava.io.IOException;2importjava.net.InetSocketAddress;3importjava.nio.ByteBuffer;4importjava.nio.channels.*;5importjava.nio.charset.Charset;6importjava.util.* 查看详情

第87题java高级技术-网络编程6(简易聊天室1:运行服务器程序,等待客户端连接)

回城传送–》《JAVA筑基100例》文章目录零、前言一、题目描述二、解题思路三、代码详解四、推荐专栏五、示例源码下载零、前言​今天是学习JAVA语言打卡的第87天,每天我会提供一篇文章供群成员阅读(不需要订阅付钱),... 查看详情

第87题java高级技术-网络编程6(简易聊天室1:运行服务器程序,等待客户端连接)(代码片段)

回城传送–》《JAVA筑基100例》文章目录零、前言一、题目描述二、解题思路三、代码详解四、推荐专栏五、示例源码下载零、前言​今天是学习JAVA语言打卡的第87天,每天我会提供一篇文章供群成员阅读(不需要订阅付... 查看详情

简易的命令行入门教程:(代码片段)

Git全局设置:gitconfig--globaluser.name"**"gitconfig--globaluser.email"**@163.com"创建git仓库:mkdir**--democd**--demogitinittouchREADME.mdgitaddREADME.mdgitadd.gitcommit-m"firstcommit"gitcommit#gitremoteaddorigin**********gitpush-uoriginmaster已有仓库?cdexist... 查看详情

用运gui实现简易的聊天室客户端

查看详情

Messer npm - Facebook Messenger 的命令行聊天。这个怎么运作?

】Messernpm-FacebookMessenger的命令行聊天。这个怎么运作?【英文标题】:Messernpm-comandlinechatforFacebookMessenger.Howitworks?【发布时间】:2019-07-1119:11:02【问题描述】:您好,上次我想编写自己的Messenger客户端,但在文档中我只看到有关... 查看详情

colmap简易教程(命令行模式)

参考技术A完整的multiviewstereopipeline会有以下步骤1.structurefrommotion(SfM)==>cameraparameters,sparsepointcloud2.multiviewstereo(MVS)==>depthmap,densepointcloud3.surfacereconstruction(SR)==>poissonordelaunyreconstruction,mesh4.texturemapping(TM)==>getmeshwithtextureCOLMAP... 查看详情

docker搭建swoole简易聊天室(代码片段)

docker搭建swoole的简易聊天室首先pull镜像dockerpulldocker.io/kong36088/nginx-php7-swoole创建容器dockerrun--name自己创建的名字-p9501:9501-p8089:80-d-itkong36088/nginx-php7-swoole/bin/bash进入容器dockerexec-it容器名字或id/bin/bash进 查看详情