如何在Linux系统中搭建一个简单的聊天室

基于Linux的多用户聊天室

作者:Cabin_V

本篇文章是我大三下学期嵌入式系统设计课程中的期末大作业,时限是两个星期。刚开始拿到这个题目的时候都愣住了,觉得时间太少(当时还有其他课程的大作业)难度偏高而且在此之前接触到的知识较少。所以在网上搜索大量文章,其中SQList具体看的这个老哥,socket具体看的是这位老哥,再根据自己的需求,配合部分代码完成以下文章。(可能还引用了一些老哥的文章但可能我记不得了,如果有此类情况发生请立即联系我,我立马做出相应处理!第一次写文章,多多包涵!)

本文主要讲述了如何在Linux操作系统下,构建一个基于多线程模式的多用户聊天室,其中包括设计思路、流程、部分代码以及实验结果展示。基于多线程模式的多用户聊天室,使用SQLite3作为进程间的通讯手段,能够实现多机连接、多机通讯以及一些聊天软件所具备有的一些简单的功能比如私聊群聊、查看在线人数、传输文件、下线等操作。在目前使用情况来看该程序较稳定,但文件传输功能偶尔存在客户端闪退现象。

1 数据库SQList

1.1 SQLite的优点

  • 从2000年5月29日开始,SQLite就选择了C语言。直到今天,C语言也是实现SQLite软件库的最佳语言。C语言是实现SQLite最好的语言的原因包括:性能、兼容性、低依赖性、稳定性。

  • 性能 像SQLite这样被密集使用的基础库需要有很好的性能。C语言很适合写这样有性能要求的程序。C语言有时被称为“便携式汇编语言”,让开发者尽可能的接近底层硬件编码,同时保证跨平台的便携性。虽然也有其他汇编语言能和C语言的速度旗鼓相当,但却没有能和C一样通用。

  • 兼容性 目前几乎所有的系统都可以调用由C语言编写的库。比如,用Java编写的Android应用能通过adapter来使用SQLite,如果SQLite是用Java编写的,这对于Andriod会更方便。但是在IOS上应用是Objective-C或者Swift编写的,这两种语言都没有办法调用Java库。因此,如果SQLite选择用Java编写,还是存在一定的局限。

  • 低依赖性 用C原因来编写库不会在运行时有太多的依赖。在最小的配置下,SQLite只需要C标准库里的:mencmp()、mencpy()、menmove()、memset()、strcmp()、strlen()、strncmp(),在更复杂的配置下,如文件传输,SQLite还可能用到malloc(),free()和一些操作系统接口来打开、读取、写入和关闭文件。但即使这样,依赖的数量也非常小。

  • 稳定性 这里说的稳定性是指语言的稳定性。C语言虽然比较老旧,但却很适合开发像SQLite这样更注重长期稳定的模块。

1.2 SQLite的程序中的应用

  • SQLite还提供了很多很方便的C语言API函数接口,本程序中包含sqlite*数据库,sqlite3_open()、sqlite3_exec()函数。其中sqlite3_open()为打开或创建数据库函数,sqlite3_exec()是执行sql语句函数。以下展示部分应用:

Server.c中main()中部分代码

ret = sqlite3_open("chat.db",&db);         //建数据库
if(ret != SQLITE_OK)
    {
        printf("数据库打开失败!");
    }
sqlite3_exec(db,"create table account(username text primary key,password text,offline_msg text);",NULL,NULL,&errmsg);//建注册信息表

2 通讯方式Socket

2.1 网络中进程之间的通讯方式

  • 网络中进程之间的通讯是通过TCP/IP协议来唯一标识一个进程,网络层的“IP地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序。这样利用三元组(IP地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。

MyHead.h中关于IP和端口的定义

#define MYPORT 6656
#define MYADDR "192.168.31.129"
  • 使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都采用的是socket。

2.2 什么是Socket?

  • Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“open->write/read->close”模式来操作。Socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

2.3 Socket的基本操作

2.3.1 socket()函数

  • socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

Server.c中socket函数的使用

socketfd = socket(AF_INET,SOCK_STREAM,0);  //调用socket创建套接字
if(socketfd == -1)
{
    perror("socket");
    return -1;
}
printf("socket success...\n");

2.3.2 bind()函数

  • bind()函数是把一个地址族中的特定地址赋给socket。

Server.c对bind函数的使用

ret = bind(socketfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(ret == -1)
{
    perror("bind");
    return -1;
}
printf("bind success...\n");

2.3.3 listen()、connect()函数

  • 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

Server.c中listen函数的使用

ret = listen(socketfd,10);
if(ret == -1)
{
    perror("listen");
    return -1;
}
printf("listen success...\n");
  • listen()函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变成被动类型的,等待客户的连接请求。

Client.c对connect函数的使用

ret = connect(socketfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(ret == -1)
{
    perror("connect");
    return -1;
}
printf("connect success...\n");
  • connect()函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

2.3.4 accept()函数

  • TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数去接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同与普通文件的读写I/O操作。

Server.c中accept函数的使用

clientfd = accept(socketfd,(struct sockaddr*)&client_addr,&len);
if(ret == -1)
{
    perror("accept");
    return -1;
}

2.3.5 read()、write()函数

  • 在上面操作进行完后,服务器与客户端已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网络中不同进程之间的通信。

Server.c中read_msg_server()对read函数的使用

readcnt = read(clientfd,&clientrecv,sizeof(client));
if(readcnt == -1)
{
    perror("read_msg_client:read");
    return 0;
}
  • read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回值是0表示已经读到文件结束了,小于0表示出现了错误。如果错误为EINTR说明是由中断引起的,如果是ECONNREST表示网络连接出了问题。

Server.c中register_client ()对write函数的使用

writecnt = write(clientfd,&clientrecv,sizeof(client));
if(writecnt == -1)
{
    perror("client:write");
    //return -1;
}
  • write函数将buf中的nbytes字节内容写入文件描述符fd成功时返回写的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有两种可能。write的返回值大于0,表示写了部分或者是全部的数据;返回值小于0,此时出现了错误。如果错误为EINTR表示在写的时候出现了中断错误,如果为EPIPE表示网络连接出现了问题。

2.3.6 close()函数

  • 在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。

Server.c中read_msg_server()对close函数的使用

close(clientfd):
  • close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

2.4 socket在TCP中的三次握手建立连接

  • 从下图可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K,ACK J+1,这时accept进入阻塞状态;客户端接收到服务器的SYN K,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
socket中发送的TCP三次握手

2.5 socket在TCP中的四次握手释放连接

  • 在某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M,另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据,一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N,接收到这个FIN的源发送端TCP对它进行确认。
socket中发送的TCP四次握手

3 多机连接

3.1 注册和登录

  • 用户注册和登录需要在客户端和服务器握手成功的基础上进行。
注册成功界面
  • 当我们在客户端输入注册信息时,客户端会向服务器发送注册信息,当服务器成功接收注册信息时,会将注册信息以链表的形式插入到数据库中,然后向客户端发送注册成功的信息。


    登录界面(左)、登录成功界面(中)和登录失败界面(右)

3.2 下线

  • 下线的原理在前面socket在TCP中的四次握手释放连接有提及到,实现其功能也比较简单,下图展示下线功能的部分代码。

Server.c中下线功能部分代码

p = head;
struct users *prev = NULL;
while(p->next != NULL)
{
    prev = p;        //prev保存p点前一个结点的地址
    p = p->next;
            
    if(strcmp(p->username,clientrecv.username) == 0)    //找到该用户
    {
        if(p->next == NULL)     //尾结点
        {
            prev->next = NULL;
            free(p);
            //p = NULL;        //NULL->next != NULL 出现段错误 
            clientrecv.flag = OFF_LINE_SUCCESS;
            writecnt = write(clientfd,&clientrecv,sizeof(client));
            if(writecnt == -1)
            {
                perror("view_friends");
                return 0;
            }
            break;
        }
        else
        {
            prev->next = p->next;
            free(p);
            //p = NULL;
            clientrecv.flag = OFF_LINE_SUCCESS;
            writecnt = write(clientfd,&clientrecv,sizeof(client));
            if(writecnt == -1)
            {
                perror("view_friends");
                return 0;
            }
            break;
        }
    }
}

4 多机通讯

4.1 获取在线好友列表

  • 客户端向服务器发送查询指令,当服务器接收指令成功时,会获取链表中的信息和链表长度返回给客户端,再通过客户端显示链表信息。
查看在线好友功能展示

4.2 私聊和群聊

  • 选择私聊功能时,发起私聊的客户端会发送私聊对象的用户名以及聊天内容至服务器,服务器在接收到信息时会判断该用户是否存在和是否在线。若用户不存在或不在线,则无法聊天,若用户存在且在线,则服务器根据接收到的用户名发送对应的聊天内容到其界面中。
私聊失败界面(左)、私聊界面(右)
  • 而选择群聊功能时,发起私聊的客户端会将聊天内容发送至服务器,服务器在接收到信息时会将信息发送给出了发起者之外的所有在线用户,显示在界面中。
群聊发起者界面(左)、群聊接收者界面(右)

4.3 文件传输

  • 选择文件传输功能时,发起者客户端会发送文件对象的用户名以及文件内容至服务器,服务器在接收到信息时会判断该用户是否存在和是否在线。若用户不存在或不在线,则无法接收,若用户存在且在线,则服务器根据接收到的用户名发送对应的文件到其客户端中且保存在客户端对应所在的文件夹中。
文件发送者界面(左)、文件接收者界面(右)

5 结论

  • 本次作业要求我们基于Linux操作系统,以C/S模式作为软件系统体系结构,构建一个多线程模式的多用户聊天室。在前面两次实验中,我们编写过多进程程序和多线程程序,在经过对比之后发现多线程程序更佳。为了让程序看上去更像是一个聊天软件,我在很多界面中通过字符或者排版的方式,特别是在私聊和群聊,加了很多细节在上面,比如在私聊时,很清晰能够看出消息发送者以及消息发送时间,而且每段聊天记录间隔清晰明显。

  • 但我觉得本次作业还有很多需要改进的地方,比如在前面一开始说到传输文件的问题,还有就是在退出一些功能时,需要输入相应的字符串才能离开界面,若要是能通过某个按键实现退出功能,则更为方便且人性化。而且本次实验中功能太单一,只是在老师要求的基础上添加了修改密码的选项,远不能达到一个聊天室的水平。

  • 感谢观看!


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335