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

基于Linux的多用户聊天室

作者:Cabin_V

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

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

文章目录

  • 基于Linux的多用户聊天室
    • 1 数据库SQList
      • 1.1 SQLite的优点
      • 1.2 SQLite的程序中的应用
    • 2 通讯方式Socket
      • 2.1 网络中进程之间的通讯方式
      • 2.2 什么是Socket?
      • 2.3 Socket的基本操作
        • 2.3.1 socket()函数
        • 2.3.2 bind()函数
        • 2.3.3 listen()、connect()函数
        • 2.3.4 accept()函数
        • 2.3.5 read()、write()函数
        • 2.3.6 close()函数
      • 2.4 socket在TCP中的三次握手建立连接
      • 2.5 socket在TCP中的四次握手释放连接
    • 3 多机连接
      • 3.1 注册和登录
      • 3.2 下线
    • 4 多机通讯
      • 4.1 获取在线好友列表
      • 4.2 私聊和群聊
      • 4.3 文件传输
    • 5 结论

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返回,至此三次握手完毕,连接建立。

如何在Linux系统搭建一个简单的用户聊天室_第1张图片

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

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

如何在Linux系统搭建一个简单的用户聊天室_第2张图片

3 多机连接

3.1 注册和登录

​ 用户注册和登录需要在客户端和服务器握手成功的基础上进行。

如何在Linux系统搭建一个简单的用户聊天室_第3张图片

​ 当我们在客户端输入注册信息时,客户端会向服务器发送注册信息,当服务器成功接收注册信息时,会将注册信息以链表的形式插入到数据库中,然后向客户端发送注册成功的信息。

如何在Linux系统搭建一个简单的用户聊天室_第4张图片

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 获取在线好友列表

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

如何在Linux系统搭建一个简单的用户聊天室_第5张图片

4.2 私聊和群聊

​ 选择私聊功能时,发起私聊的客户端会发送私聊对象的用户名以及聊天内容至服务器,服务器在接收到信息时会判断该用户是否存在和是否在线。若用户不存在或不在线,则无法聊天,若用户存在且在线,则服务器根据接收到的用户名发送对应的聊天内容到其界面中。

如何在Linux系统搭建一个简单的用户聊天室_第6张图片

​ 而选择群聊功能时,发起私聊的客户端会将聊天内容发送至服务器,服务器在接收到信息时会将信息发送给出了发起者之外的所有在线用户,显示在界面中。

如何在Linux系统搭建一个简单的用户聊天室_第7张图片

4.3 文件传输

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

如何在Linux系统搭建一个简单的用户聊天室_第8张图片

5 结论

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

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

​ 感谢观看!
线程程序,在经过对比之后发现多线程程序更佳。为了让程序看上去更像是一个聊天软件,我在很多界面中通过字符或者排版的方式,特别是在私聊和群聊,加了很多细节在上面,比如在私聊时,很清晰能够看出消息发送者以及消息发送时间,而且每段聊天记录间隔清晰明显。

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

​ 感谢观看!
如何在Linux系统搭建一个简单的用户聊天室_第9张图片

你可能感兴趣的:(Linux)