目錄
-
-
- 前言
- 一、聊天室的實驗内容
- 二、逐個功能的簡單分析
- 三、系統功能子產品分解圖
-
- 1.服務端功能子產品圖
- 2.用戶端功能子產品圖
- 3.守護程序功能子產品圖
- 四、功能子產品流程圖
-
- 1.服務端流程圖
- 2.用戶端流程圖
- 3.守護程序流程圖
- 五、實驗截圖
- 六、問題及解決
- 七、參考文獻
- 附錄
-
前言
由于疫情原因,在家上了一學期的課,本次作業是作為“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 在服務端檢視系統日志
六、問題及解決
- 對socket定義及格式、select函數使用、檔案描述符等的定義印象模糊:檢視《Linux程式設計》[1]以及大量部落格進行學習,這裡隻是概念及使用的問題。
- 由誰實作需求的核心功能:這裡的“核心功能”指的是驗證登入,在背景注冊資訊等。我最初規劃時将這些功能并入到用戶端,但轉念一想,這樣實作雖然簡單,但并不現實——用戶端顯然不能去通路一些重要的檔案——若這些通路功能在用戶端實作,那系統肯定是不安全的。如果把檔案存儲形式轉化為資料庫存儲,那用戶端豈不是可以直接通路資料庫了。是以應該通過用戶端向服務端發送相應請求,由服務端對這些請求進行相應的處理操作。
- 資料格式的設計:在編寫用戶端時,由于有功能需要發送使用者名及密碼到服務端做驗證,是以我在用戶端連續兩次send,并在服務端的開頭連續兩次recv——這直接導緻了聊天系統每隔一次才轉發一次資料。後來通過其他人的問答[6]才知道,服務端單獨接收連續兩條資料時,可以在用戶端按照一定格式拼裝兩條輸入為一條,這樣在服務端就可以用一條recv接收,并根據用戶端的拼裝格式在服務端解析。
- 使用Ctrl+C退出服務端,導緻守護程序無法記錄服務端關閉時間:這算是老師講要求時我沒聽完全,原來是可以在服務端界面輸入選項正常退出系統。最初我是打算先啟動daemon,再啟動服務端,但這樣的話涉及到程序通信時,服務端也會作為用戶端向daemon發送登入消息。後來請教老師之後,才明白是自己設計思路出了問題——應該在服務端設計面向管理者的界面,在用戶端設計面向客戶的界面,daemon作為一個特殊的用戶端程序,在啟動服務端後自動啟動,在關閉服務端後記錄日志并自動關閉。
-
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