摘 要: 為了讓Linux系統(tǒng)用戶得到更多網(wǎng)絡(luò)聊天的支持,基于套接字編程方法設(shè)計(jì)了一種應(yīng)用在Linux的網(wǎng)絡(luò)聊天系統(tǒng),它具有群聊、私聊、查詢、信息加密等功能,并在Fedora 10系統(tǒng)上做了相應(yīng)驗(yàn)證性實(shí)驗(yàn)。實(shí)驗(yàn)結(jié)果表明其完全達(dá)到了設(shè)計(jì)的預(yù)設(shè)要求,具有較強(qiáng)的實(shí)用性。
關(guān)鍵詞: 套接字; TCP; 客戶端; 服務(wù)端
中圖分類號(hào): TN711?34; TP311.1 文獻(xiàn)標(biāo)識(shí)碼: A 文章編號(hào): 1004?373X(2013)03?0051?04
隨著互聯(lián)網(wǎng)的發(fā)展,人與人之間的交流方式變得多樣化。網(wǎng)絡(luò)聊天就是其中一種新起的交流方式,其不分地域,具有實(shí)時(shí)性,只要有網(wǎng)絡(luò)和聊天系統(tǒng)就可以進(jìn)行交流。作為開源的操作系統(tǒng),Linux自然擁有不少的用戶,特別在服務(wù)器的應(yīng)用上更是廣泛。而如今大多數(shù)的網(wǎng)絡(luò)聊天系統(tǒng)都是針對(duì)Windows系統(tǒng)開發(fā)的,針對(duì)Linux系統(tǒng)的相對(duì)比較少。因此本文設(shè)計(jì)了一種基于套接字編程方法針對(duì)Linux的網(wǎng)絡(luò)聊天系統(tǒng),其具有最基本的聊天功能:群聊和私聊,除此之外,本系統(tǒng)還添加了查詢、信息加密、內(nèi)容發(fā)送時(shí)間、服務(wù)器顯示信息等功能。
1 系統(tǒng)原理
1.1 套接字的概述
套接字(Socket)是網(wǎng)絡(luò)通信的基礎(chǔ),是支持TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元[1]。套接字可以被認(rèn)為是網(wǎng)絡(luò)通信連接中的端點(diǎn),接入局域網(wǎng)中的每臺(tái)主機(jī)都是用套接字來標(biāo)識(shí),有了套接字,才能保證發(fā)送的數(shù)據(jù)能傳送到正確的主機(jī)上。
在網(wǎng)絡(luò)通信中,主要是通過IP地址、傳輸協(xié)議(TCP/UDP)、端口三個(gè)參數(shù)來區(qū)別網(wǎng)絡(luò)中不同主機(jī)之間的通信[2]。Linux中的一切都是文件,內(nèi)核是利用文件描述符(file descriptor)來訪問文件,在網(wǎng)絡(luò)操作中也是通過文件描述符來進(jìn)行發(fā)送和接收數(shù)據(jù)的[3]。套接字就是結(jié)合以上三個(gè)參數(shù),與一個(gè)文件描述符綁定得到的,從而主機(jī)就可以通過創(chuàng)建套接字與其他主機(jī)進(jìn)行信息的傳遞。
1.2 TCP/IP協(xié)議中套接字的類型
在TCP/IP協(xié)議中,有3種常用的套接字類型[2,4]:流套接字(SOCK_STREAM)、數(shù)據(jù)包套接字(SOCK_DGRAM)、原始套接字(SOCK_RAW)。字節(jié)流套接字使用了傳輸控制協(xié)議,即TCP(The Transmission Control Protocol)協(xié)議,因此其用于提供面向連接的、可靠的數(shù)據(jù)傳輸服務(wù),該服務(wù)可以實(shí)現(xiàn)順序、無差錯(cuò)、不重復(fù)的流傳輸;而數(shù)據(jù)包套接字使用的是用戶數(shù)據(jù)報(bào)協(xié)議,即UDP(User Datagram Protocol)協(xié)議,該協(xié)議是一種無連接的、不可靠的傳輸層協(xié)議,因此其提供的傳輸服務(wù)是不能保證數(shù)據(jù)傳輸?shù)目煽啃?,在傳輸過程中有可能出現(xiàn)數(shù)據(jù)丟失、數(shù)據(jù)重復(fù)以及不能順序接收數(shù)據(jù)的情況,其只適合于可靠性較高的局域網(wǎng);原始套接字允許對(duì)較低層次的協(xié)議直接訪問,比如IP、 ICMP協(xié)議,它常用于檢驗(yàn)新的協(xié)議實(shí)現(xiàn),或者訪問現(xiàn)有服務(wù)中配置的新設(shè)備。
本系統(tǒng)要實(shí)現(xiàn)聊天系統(tǒng)的功能,就涉及到字符串?dāng)?shù)據(jù)的傳輸,數(shù)據(jù)的順序及可靠性要得到充分保證,因此本系統(tǒng)決定使用的是字節(jié)流套接字。
1.3 TCP協(xié)議
TCP是一種面向連接的、可靠的傳輸層協(xié)議[5]。TCP協(xié)議在網(wǎng)絡(luò)層IP協(xié)議的基礎(chǔ)上,向用戶進(jìn)程提供可靠性、全雙工的數(shù)據(jù)流傳輸[6]。TCP在進(jìn)行數(shù)據(jù)傳遞之前,必須先建立傳輸連接;在數(shù)據(jù)傳輸完畢后,需要釋放傳輸連接。
1.3.1 TCP連接的建立
TCP協(xié)議建立連接必須經(jīng)過三次握手[7](見圖1)。在握手之前,服務(wù)器必須一直處于監(jiān)聽的狀態(tài),等待客戶的連接請求。第一次握手:客戶向服務(wù)器發(fā)送SYN包,并等待服務(wù)器的響應(yīng),SYN是同步序列編號(hào);第二次握手:服務(wù)器必須對(duì)客戶發(fā)送過來的SYN包進(jìn)行確認(rèn),同時(shí)向客戶發(fā)送一個(gè)SYN包,即此時(shí)發(fā)送給客戶的是SYN包+ACK包;第三次握手:客戶收到服務(wù)器發(fā)送過來的SYN包和ACK包后,向服務(wù)器發(fā)送對(duì)SYN包確認(rèn)的ACK包。這樣三次握手完成,傳輸連接就此建立,之后就可以在連接上進(jìn)行數(shù)據(jù)的傳送。
1.3.2 TCP連接的釋放
當(dāng)數(shù)據(jù)傳輸結(jié)束后,TCP會(huì)通過四次握手[7](見圖2)來終止傳輸連接。第一次握手:客戶通過發(fā)送FIN包向服務(wù)器提出釋放連接的請求,并等待服務(wù)器響應(yīng)的確認(rèn);第二次握手:服務(wù)器收到來自客戶端的FIN包后會(huì)向其發(fā)送一個(gè)ACK包,表示同意釋放連接;第三次握手:服務(wù)器向客戶發(fā)送FIN包來表示其要關(guān)閉連接;第四次握手:客戶向服務(wù)器發(fā)送ACK包來確認(rèn)收到服務(wù)器送過來的FIN包,至此,TCP連接徹底終止。
1.4 客戶/服務(wù)器模型
在客戶/服務(wù)器模型中,多個(gè)相互通信的主機(jī)都作為客戶端,與服務(wù)器進(jìn)行連接,并以服務(wù)器作為數(shù)據(jù)中轉(zhuǎn)來進(jìn)行客戶間的信息傳遞[8]。從另一個(gè)角度來看,多個(gè)客戶端之間的通信實(shí)際上就是服務(wù)器與客戶端之間端對(duì)端的通信。本系統(tǒng)設(shè)計(jì)的是多用戶聊天系統(tǒng),所用到的就是客戶/服務(wù)器模型,因此需要分別用C語言編寫服務(wù)端程序與客戶端程序。服務(wù)端與客戶端利用TCP連接進(jìn)行通信的程序流程圖[9?10]如圖3所示,其中方框里的是套接字函數(shù)。
2 系統(tǒng)設(shè)計(jì)的具體實(shí)現(xiàn)
2.1 常用套接字函數(shù)的介紹
(1)socket函數(shù):原型是int socket(int family,int type,int protocol),該函數(shù)功能是指明了協(xié)議族與套接字類型,其中本系統(tǒng)設(shè)置其參數(shù)family=AF_INET,type=SOCK_STREAM,protocol=0,表明使用IPv4協(xié)議,字節(jié)流套接字。該函數(shù)在成功時(shí)返回一個(gè)小的非負(fù)整數(shù)值;出錯(cuò)時(shí),返回-1。
(2)connect函數(shù):原型是int connect(int sockfd,SA *serv_addr,int addrlen),該函數(shù)功能是客戶用來建立與TCP服務(wù)器的連接,其中參數(shù)sockfd是由socket函數(shù)返回的套接字文件描述符,第二、三個(gè)參數(shù)分別是一個(gè)指向套接字地址結(jié)構(gòu)的指針和該結(jié)構(gòu)的大小,serv_addr是保存目的地址端口和IP地址,其成功時(shí)返回值為0,出錯(cuò)時(shí)返回-1。
(3)bind函數(shù):原型是int bind (int sockfd,const SA*my_addr,int addrlen),該函數(shù)功能是給套接字分配一個(gè)本地協(xié)議地址,其中sockfd也是由socket函數(shù)返回的套接字文件描述符,my_addr是指向套接字地址結(jié)構(gòu)的指針,也即保存IP地址和端口信息,addrlen為該數(shù)據(jù)結(jié)構(gòu)的大小。其成功時(shí)返回值為0,出錯(cuò)時(shí)返回-1。該函數(shù)主要用在服務(wù)端。
(4)linsten函數(shù):原型是int listen(int sockfd,int backlog),其功能是把一個(gè)未連接的套接字轉(zhuǎn)換成一個(gè)被動(dòng)套接字,指示內(nèi)核要接受該套接字的連接請求,其中sockfd是socket函數(shù)成功調(diào)用返回的套接字文件描述符,backlog指明該套接字中隊(duì)排隊(duì)的最大連接個(gè)數(shù)。成功時(shí)返回0,失敗時(shí)返回-1。該函數(shù)僅由服務(wù)器調(diào)用,并且一般都是在socket函數(shù)跟bind函數(shù)后調(diào)用。
(5)accept函數(shù):原型是int accept(int sockfd,SA*
cliaddr, int* addrlen),該函數(shù)功能是在已完成隊(duì)列連接隊(duì)列隊(duì)頭返回下一個(gè)已完成連接,如果已完成隊(duì)列為空,則進(jìn)程會(huì)處于阻塞狀態(tài)。參數(shù)sockfd跟其上面提到的一樣,是返回的套接字文件描述符,而cliaddr,addrlen分別是函數(shù)成功調(diào)用返回的客戶進(jìn)程協(xié)議地址和地址的大小,除以上返回外,該函數(shù)還會(huì)返回新套接字文件描述符(也就是已連接的套接字),失敗時(shí)返回-1。該函數(shù)也是由TCP服務(wù)器調(diào)用。
(6)close函數(shù):原型是int close(int sockfd),該函數(shù)功能是關(guān)閉套接字,并終止TCP連接。參數(shù)sockfd是要關(guān)閉的套接字,成功時(shí)返回0,失敗時(shí)返回-1。
(7)輸入輸出函數(shù):由于本系統(tǒng)是主機(jī)跟主機(jī)之間的文字信息交互的平臺(tái),涉及到客戶端和服務(wù)端輸入輸出的操作,因此需要使用到輸入輸出函數(shù)。
套接字的輸入函數(shù):readline函數(shù)從套接字讀入一串字符,并將該字符串存入到字符數(shù)組中。函數(shù)成功時(shí)返回輸入字符串的長度,錯(cuò)誤時(shí)返回-1。
套接字的輸出函數(shù):writen函數(shù)把字符數(shù)組中的字符串寫入到套接字中,成功時(shí)返回寫入字符串的長度,錯(cuò)誤時(shí)返回-1。
鍵盤輸入函數(shù):fgets函數(shù)接收用戶從鍵盤輸入的字符串,并將該字符串存入字符數(shù)組中。
終端輸出函數(shù):fputs函數(shù)將字符串輸出到終端,顯示字符串。
(8)select函數(shù):使用輸入輸出函數(shù)會(huì)使客戶出現(xiàn)阻塞于標(biāo)準(zhǔn)輸入或套接字輸入的情況,也就是說程序會(huì)等待用戶鍵盤輸入或者等待套接字的輸入[10]。當(dāng)服務(wù)器進(jìn)程終止后,客戶端仍阻塞于標(biāo)準(zhǔn)輸入,此時(shí)客戶再次輸入字符串時(shí)才會(huì)發(fā)現(xiàn)服務(wù)器已經(jīng)終止,但是服務(wù)器并沒有告知客戶端TCP服務(wù)器已關(guān)閉了連接,因此客戶端不能單純阻塞于標(biāo)準(zhǔn)輸入和套接字某個(gè)特定源的輸入,而應(yīng)該阻塞于其中任何一個(gè)源的輸入。為了解決這個(gè)問題,就引入了select函數(shù)。select函數(shù)會(huì)等待多個(gè)事件中的任何一個(gè)發(fā)生,當(dāng)某個(gè)事件或多個(gè)事件發(fā)生或經(jīng)歷一定時(shí)間時(shí),它會(huì)告知內(nèi)核進(jìn)行處理。select函數(shù)可以設(shè)置四種感興趣的事件集,分別是讀、寫或異常條件,以及等待多長時(shí)間。本系統(tǒng)調(diào)用select函數(shù),主要用于檢查可讀性的描述字集,也就是當(dāng)套接字或標(biāo)準(zhǔn)輸入可讀時(shí),才會(huì)喚醒內(nèi)核去處理。
(9)shutdown函數(shù):輸入的文件結(jié)束符并不意味著已完成了從套接字的讀入,可能仍有請求在去往服務(wù)器的路上或是在去往客戶的路上仍有應(yīng)答,因此就需要一種方法來關(guān)閉TCP連接的一半,這就是由shutdown函數(shù)來完成[10]。shutdown函數(shù)可以終止數(shù)據(jù)傳送的兩個(gè)方向:讀或?qū)?,或其中任一方向:讀或?qū)?。本系統(tǒng)主要在客戶端使用到shutdown函數(shù),當(dāng)收到服務(wù)器要關(guān)閉連接的信息時(shí),就用shutdown函數(shù)關(guān)閉連接的寫方向,從而避免客戶繼續(xù)發(fā)送信息,同時(shí)由于保留了連接的讀方向,客戶仍能收到在傳輸路上的信息。
2.2 各功能的具體實(shí)現(xiàn)思想
本系統(tǒng)主要的工作是編寫客戶端和服務(wù)端程序,其中服務(wù)器是數(shù)據(jù)中轉(zhuǎn)的主要承擔(dān)者,其工作量比較大,因此系統(tǒng)的關(guān)鍵在于服務(wù)端程序的設(shè)計(jì)。客戶端程序的主要任務(wù)是完成用戶、套接字的輸入輸出。而服務(wù)端程序不但要完成套接字的輸入輸出,而且要完成實(shí)現(xiàn)各功能的設(shè)計(jì)。
2.2.1 群聊功能
本系統(tǒng)是設(shè)計(jì)聊天系統(tǒng),群聊是其最基本的功能。在服務(wù)器中,每個(gè)與服務(wù)端連接的客戶都用不同的套接字來標(biāo)識(shí),而且每個(gè)客戶都會(huì)有自定義的用戶名。群聊是采取廣播的形式,當(dāng)某個(gè)客戶要發(fā)送信息給大家時(shí),服務(wù)器調(diào)用time函數(shù)來獲得當(dāng)前時(shí)間,并將此時(shí)間與原信息、信息來源者的用戶名結(jié)合在一起,一并發(fā)送給已連接上的客戶,這樣就完成了一次信息的廣播發(fā)送。
2.2.2 私聊功能
私聊功能是客戶與客戶之間單對(duì)單的聊天,本系統(tǒng)主要用“目標(biāo)用戶名”的指令來表明私聊,其中目標(biāo)用戶名就是想要聊天的對(duì)象用戶名,在指令后加上要發(fā)送的內(nèi)容就可以向?qū)Ψ絺魉托畔?。服?wù)器是用套接字來標(biāo)識(shí)不同客戶的,而且套接字是惟一的,只要找到與用戶名相對(duì)應(yīng)的套接字就能準(zhǔn)確地將內(nèi)容發(fā)送到目的客戶。服務(wù)端程序就是先從指令中提取用戶名,再根據(jù)用戶名找到相應(yīng)的套接字,最后就將內(nèi)容發(fā)給對(duì)應(yīng)的套接字。
2.2.3 加密功能
為了信息的保密性,本系統(tǒng)對(duì)內(nèi)容的傳送都進(jìn)行了加密的操作。客戶端、服務(wù)端在向套接字發(fā)送數(shù)據(jù)時(shí),都會(huì)將數(shù)據(jù)進(jìn)行加密,所謂的加密就是將數(shù)據(jù)倒序或者加上某個(gè)數(shù)之類的操作。而在套接字接收到數(shù)據(jù)時(shí)要先對(duì)數(shù)據(jù)進(jìn)行解密,后才進(jìn)行后續(xù)的工作。解密是加密的逆過程,兩者必須要對(duì)應(yīng)才能使數(shù)據(jù)復(fù)原正確。由于客戶端跟服務(wù)端都有對(duì)套接字進(jìn)行輸入輸出的操作,因此兩者的程序中都包含了對(duì)數(shù)據(jù)加密跟解密的程序塊。
2.2.4 查詢在線用戶功能
當(dāng)客戶使用當(dāng)輸入指令“#”,就可以查詢當(dāng)前在線用戶。要實(shí)現(xiàn)此功能是較為簡單的,只要在服務(wù)器中判斷是否“#”指令,如果是的話,服務(wù)器通過套接字來查詢已連接的用戶名,并將各用戶名結(jié)合到同一條信息上,之后發(fā)送給輸入指令的客戶。
3 實(shí)驗(yàn)效果
為了測試本系統(tǒng)是否達(dá)到設(shè)計(jì)要求,在fedora 10系統(tǒng)上做了實(shí)驗(yàn)。該實(shí)驗(yàn)需要打開四個(gè)控制終端,一個(gè)運(yùn)行服務(wù)端程序,三個(gè)運(yùn)行客戶端程序。在終端上輸入./tcpserver就能啟動(dòng)服務(wù)程序,輸入./tcpclient 127.0.0.1啟動(dòng)客戶程序,其中127.0.0.1是回送地址,指本地機(jī),一般用來測試使用。