Linux下socket多人聊天室

Linux环境编程 专栏收录该内容
8 篇文章 0 订阅

前言

由于疫情原因,在家上了一学期的课,本次作业是作为“Linux程序设计”的期末考核而布置的,代替了原本的线上答题考试,对于我这种比较喜欢动手的菜鸡来说,还是很舒服的。


一、聊天室的实验内容

本作业实现一个基于Linux的模拟即时通信系统,要求实现以下功能:
1、模拟即时通信系统可以实现多人同时在线聊天功能;
2、在线聊天用户登录本系统需输入用户名和密码;
3、本系统需能够查询历史聊天记录;
4、本系统运行后,需启动一个守护进程,该守护进程记录本系统启动和关闭的时间,每个用户登录和退出时间;(日志)
5、需创建本系统的Makefile管理文件,管理系统源码。


二、逐个功能的简单分析

1.实现多人同时在线聊天:一个server,多个client,在server中定义客户端结构体,客户端的实体记录客户端状态,用户账号以及文件描述符。聊天时服务端不断监听客户端的状况(while(1)),并对客户端的请求或者动作进行相应的处理。
2.验证、注册功能:在服务端实现。客户端向服务端发送用户登录的用户名以及密码后,服务端读取文件并检索匹配,返回相应的状态(匹配成功、匹配失败、注册新用户的用户名已存在等)。
3.保存、查询历史聊天记录:客户端实现。为防止非正常退出情况下,用户聊天记录可能不被及时保存到文件而导致聊天记录丢失,因此在每次读取文件后均对文件缓冲区使用fflush(fp)的方式进行刷新。
4.守护进程记录系统日志:参考《Linux程序设计》[1] 中关于守护进程编写一节,将守护进程与创建其的父进程、所在文件等“脱离关系”,并由服务器get到守护进程的文件描述符,将系统、用户的登录退出状态发送给守护进程,由守护进程进行记录。
5.Makefile文件编写,参考[1]中关于如何编写Makefile一节。


三、系统功能模块分解图

1.服务端功能模块图

在这里插入图片描述

图3.1 服务端功能模块图

服务端主要完成的工作是对客户端的请求进行对应的处理,以及对客户端发送的聊天记录进行处理转发。同时,在服务端选择开启聊天系统的同时,应当启动一个对应的守护进程以便于在后台记录聊天系统的开启关闭时间和用户的登录推出时间。因此服务端具有开启、关闭聊天系统,注册用户信息,验证用户信息,转发聊天记录以及开启守护进程的功能。

2.客户端功能模块图

在这里插入图片描述

图3.2 客户端功能模块图

客户端主要完成的工作是注册用户,登录用户以及查看聊天记录。需要说明的是,在用户登录且信息成功匹配后,才能进行聊天记录的查看。此外,考虑到用户成功登录后会有退出的需求,客户端开启后会有关闭的需求,因此,客户端功能模块中还具有退出用户登录以及退出客户端的功能。

3.守护进程功能模块图

在这里插入图片描述

图3.1 守护进程功能模块图

守护进程用户在服务端开启后,实时记录聊天系统的开启、关闭时间,以及某一用户的登录、退出时间。因此,守护进程具有持续接收服务端信息,以及将接收的信息写入到系统日志的功能。


四、功能模块流程图

由于服务端逻辑稍复杂,因此这里对服务端的流程图进行解释。

1.服务端流程图

当打开服务端后,系统会提示用户可以输入三个选项。
(1)当输入为 1 时,开启聊天系统,正式建立socket连接并启动守护进程以便将系统开闭时间,用户登退时间记录在系统日志。此时不断循环以随时监听客户端发送的信息。将server端套接字文件描述符、所有在线客户端的文件描述符加入到文件描述符集,同时将标准输入加入到文件描述符集,以便捕获非阻塞输入。
若有管理员在服务端开启后输入选项 2,则进行相应的善后处理,将所有已开启客户端中的在线用户下线,将下线信息传递给守护进程并由守护进程写入到系统日志。同时将服务器的关闭时间写入到系统日志,由于进程间传递数据会有短暂延迟,为避免服务器传递数据后立即关闭导致的守护进程无法正常写入日志的问题,因此延迟两秒关闭服务器。
若聊天系统开启后无管理员输入,则持续监听客户端发送的消息,并对消息类型进行判断,若为正常的聊天消息,则将聊天信息转发至登陆者非信息发送方的客户端,否则判断消息发送者是客户端还是守护进程,若为守护进程发送的消息,则证明系统首次启动(守护进程只在启动时向服务器发送一条消息,此后一直处于接收状态),那么将系统启动信息发送给守护进程。若非守护进程,进而判断是注册还是登陆,若是登陆,将客户端结构体的用户在线状态置1,并填入用户信息,否则进行注册。

(2)当输入为 2 时,直接关闭服务端。

(3)当输入为 3 时,访问系统日志文件,并将系统日志读出,打印在服务端窗口。
在这里插入图片描述

2.客户端流程图

3.守护进程流程图


五、实验截图

说明:makefile文件中共有四个target,包括server、client、daemon以及clean。

图6.1 未make前的文件目录结构

2.运行批处理文件run.bat执行Makefile来编译链接程序

图6.2 对四个target执行make

3.开启服务端,登录一个客户端并注册未存在用户

图6.3 开启服务端与客户端,在客户端注册未注册的用户信息

4.注册已存在用户zhangsan

图6.4 在客户端注册已存在用户,提示user has exist

5.另外开启两个客户端,登录刚刚注册的用户以及两个已存在用户,模拟用户聊天。

图6.5 模拟多人即时通信

6.zhangsan和lisi输入指定命令#bye退出聊天,cuiyanran输入指定命令#chat record在聊天窗口查看历史聊天记录。

图6.6 在一个已登录用户的客户端查看聊天记录

7.cuiyanran退出聊天系统,此时所有用户业已退出。在服务端根据提示输入2,以退出服务器。(输入其他字符无效,程序不做处理)

图6.7 在服务端输入2以关闭服务器

8.重新开启服务端,按提示输入3,以查看系统日志。
图中圈起部分为上面的演示中系统记录日志得到的。

图6.8 在服务端查看系统日志

六、问题及解决

  1. 对socket定义及格式、select函数使用、文件描述符等的定义印象模糊:查看《Linux程序设计》[1]以及大量博客进行学习,这里只是概念及使用的问题。
  2. 由谁实现需求的核心功能:这里的“核心功能”指的是验证登录,在后台注册信息等。我最初规划时将这些功能并入到客户端,但转念一想,这样实现虽然简单,但并不现实——客户端显然不能去访问一些重要的文件——若这些访问功能在客户端实现,那系统肯定是不安全的。如果把文件存储形式转化为数据库存储,那客户端岂不是可以直接访问数据库了。因此应该通过客户端向服务端发送相应请求,由服务端对这些请求进行相应的处理操作。
  3. 数据格式的设计:在编写客户端时,由于有功能需要发送用户名及密码到服务端做验证,因此我在客户端连续两次send,并在服务端的开头连续两次recv——这直接导致了聊天系统每隔一次才转发一次数据。后来通过其他人的问答[6]才知道,服务端单独接收连续两条数据时,可以在客户端按照一定格式拼装两条输入为一条,这样在服务端就可以用一条recv接收,并根据客户端的拼装格式在服务端解析。
  4. 使用Ctrl+C退出服务端,导致守护进程无法记录服务端关闭时间:这算是老师讲要求时我没听完全,原来是可以在服务端界面输入选项正常退出系统。最初我是打算先启动daemon,再启动服务端,但这样的话涉及到进程通信时,服务端也会作为客户端向daemon发送登录消息。后来请教老师之后,才明白是自己设计思路出了问题——应该在服务端设计面向管理员的界面,在客户端设计面向客户的界面,daemon作为一个特殊的客户端进程,在启动服务端后自动启动,在关闭服务端后记录日志并自动关闭。
  5. TCP“粘包”问题:这可以说是我在编写本程序时遇到的最大问题。在解决了上述一系列问题,并自以为无错误之虞时,我发现了一个致命问题——将系统开闭时间、用户登退时间写入到系统日志后,总会有一些莫名其妙多出来的字符串独占一行(ex.0, 020, 20),且一定伴随在某条“用户登录退出时间”记录的前后出现。
    5.1 debug经历及一些推断
    于是我将守护进程的代码改成一般客户端记录日志程序,并在启动server后手动启动,分别在server端和这个“记录日志程序”输出发送/接收的串,发现server端发送的串毫无问题,可发送的同时,日志程序接收的串却多出来了0,20,020… 这样,我初步分析是日志程序接收数据除了问题。
    经过反复测试,我还发现一个很“灵异”的事——使用zhangsan登录系统时,会有一部分提示信息被吞掉,同时zhangsan登录时间竟然和服务器启动时间一模一样;在使用用户名较短的lisi、wangwu进行登录时,中间会多出一部分字符(ex. T, AT)——这正是我发送的日志格式中的部分字符("%s LOG IN AT %s", usr_name, getLocalTime())。
    联想起上面日志程序接收的串多出来的"0, 20, 020",我突然有了一个想法!类似于有时需要及时刷新输出缓冲区,socket是否也存在一个类似于“缓冲区”的空间,用于存储信息,仅当缓冲区满后,才将缓冲区中存储的信息全部发送到接收方,这样就会导致多条数据粘在一起。(计网没学好导致的坑,只能自己去猜测)。于是我故意将代码中发送用户登录信息到守护进程的部分,改为发送一个空串,启动客户端登录一个用户后,观察新生成的文件,果然,有两条一模一样的“SYSTEM START AT Tues Jun 13 18:15:10 2020”,这也就解释了为什么日志程序接收的串尾会多一些字符,同时用户登录时间竟和系统启动时间一样——并非如此,而是在用户端接收数据时,接收到的串是由用户登录+系统开启拼接而成的。
    5.2 观点印证及解决措施
    为了进一步佐证我的观点,我通过百度“socket缓冲区”,发现有网友在CSDN提出了和我近似的问题,当时有版主回应称是典型的TCP“粘包问题”,解决方案是设计自己设计数据包格式,一种典型的格式是“数据长度+分隔符+数据”,这样,即使出现了“粘包”问题,客户端通过数据格式解析接受到的数据包,也能读到正确的串。
    原来,上面我所谓的关于“socket”缓冲区的猜测,准确来说是TCP连接的问题。要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。[5]

七、参考文献

[1] 严冰,刘加海,季江民.Linux程序设计[M].浙大出版社:浙江,2012-2-1.
[2] socket通信中select函数的使用和解释, 博客园, https://www.cnblogs.com/gangzilife/p/9766292.html
[3] select系统调用与FD_SET,FD_ISSET,FD_ZERO , 博客园, https://www.cnblogs.com/aaronwxb/articles/2665507.html
[4] Linux的文件描述符, 博客园, https://www.cnblogs.com/diantong/p/10413079.html
[5] 【linux】粘包的产生和解决, CSDN, https://blog.csdn.net/xing1584114471/article/details/94592213
[6] socket怎么实现send和recv的连续发送, IIANEWS, http://www.iianews.com/zhidao/question-14303.shtml
[7] linux下C获取系统时间的方法, 博客园, https://www.cnblogs.com/zxc2man/p/7660240.html
[8] 服务器编程入门(12)守护进程, 博客园, https://www.cnblogs.com/suzhou/p/daemon.html
[9] Socket中select()的用法, CSDN, https://blog.csdn.net/gxj1680/article/details/3722771
[10] linux下C编程规范, CSDN, https://blog.csdn.net/lee244868149/article/details/38539263
[11] 【Linux的C语言】小型多人在线聊天室, bilibili, https://www.bilibili.com/video/BV12s411A75J/?p=4&t=41


附录

可在csdn上进行下载:链接:https://download.csdn.net/download/cprimesplus/12545635

  • 5
    点赞
  • 0
    评论
  • 37
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值