歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux資訊 >> Linux文化 >> Linux程式設計入門之聊天室實現

Linux程式設計入門之聊天室實現

日期:2017/2/27 12:18:02   编辑:Linux文化

某天偶然翻到以前的程序, 拿來改了改,順便寫了一點心得, 與大家共享: 如果想獲得文中的程序源碼, 請參觀碼源主頁:

http://edoc.163.net/ 或者科大鏡象:

http://202.38.79.17:8080/

一個簡單聊天室的兩種實現 (fcntl 和 select)

一個簡單的聊天室, 其功能是當在這個聊天室中的任何一個

用戶輸入一段字符之後, 室內的所有其他用戶都可以得到這一句

話. 本聊天室功能非常簡單, 感興趣的朋友可以將其功能擴展, 發

展成一個功能比較完整的聊天室, 如加上用戶認證, 用戶昵稱, 秘密

信息, semote 等功能. 這個聊天室有兩種實現方法: fcntl 和 select,

下面就這個聊天室的實現做一個介紹, 並對兩種方法進行比較.

聊天室是一個 client/server 結構的程序, 首先啟動 server,

然後用戶使用 client 進行連接. client/server 結構的優點是速度

快, 缺點是當 server 進行更新時, client 也必需更新.

首先是 初始化 server, 使server 進入監聽狀態: (為了簡潔起見,

以下引用的程序與實際程序略有出入, 下同)

sockfd = socket( AF_INET,SOCK_STREAM, 0);

// 首先建立一個 socket, 族為 AF_INET, 類型為 SOCK_STREAM.

// AF_INET = ARPA Internet protocols 即使用 TCP/IP 協議族

// SOCK_STREAM 類型提供了順序的, 可靠的, 基於字節流的全雙工連接.

// 由於該協議族中只有一個協議, 因此第三個參數為 0

bind( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));

// 再將這個 socket 與某個地址進行綁定.

// serv_addr 包括 sin_family = AF_INET 協議族同 socket

// sin_addr.s_addr = htonl( INADDR_ANY) server 所接受的所有其他

// 地址請求建立的連接.

// sin_port = htons( SERV_TCP_PORT) server 所監聽的端口

// 在本程序中, server 的 IP和監聽的端口都存放在 config 文件中.

listen( sockfd, MAX_CLIENT);

// 地址綁定之後, server 進入監聽狀態.

// MAX_CLIENT 是可以同時建立連接的 client 總數.

server 進入 listen 狀態後, 等待 client 建立連接. 此時如果有

client 進行連接時, 也要先進行網絡部分的初始化工作:

sockfd = socket( AF_INET,SOCK_STREAM,0));

// 同樣的, client 也先建立一個 socket, 其參數與 server 相同.

connect( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));

// client 使用 connect 建立一個連接.

// serv_addr 中的變量分別設置為:

// sin_family = AF_INET 協議族同 socket

// sin_addr.s_addr = inet_addr( SERV_HOST_ADDR) 地址為 server

// 所在的計算機的地址.

// sin_port = htons( SERV_TCP_PORT) 端口為 server 監聽的端口.

當 client 建立新連接時, server 使用 accept 來接受該連接:

accept( sockfd, (struct sockaddr*)&cli_addr, &cli_len);

// 在函數返回時, cli_addr 中保留的是該連接對方的信息

// 包括對方的 IP 地址和對方使用的端口.

// accept 返回一個新的文件描述符.

在 server 進入 listen 狀態之後, 我們下面分別討論兩種實現方法:

1. fcntl 方法

對一個文件描述符指定的文件或設備, 有兩種工作方式: 阻塞與非阻塞,

阻塞的意思是指, 當試圖對該文件描述符進行讀寫時, 如果當時沒有東西可讀,

或者暫時不可寫, 程序就進入等待狀態, 直到有東西可讀或者可寫為止. 而對於

非阻塞狀態, 如果沒有東西可讀, 或者不可寫, 讀寫函數馬上返回, 而不會等待.

缺省情況下, 文件描述符處於阻塞狀態. 在實現聊天室時, server 輪流查詢與各

client 建立 socket, 一旦可讀就將該 socket 中的字符讀出來並向所有其他

client 發送. 並且, server 還要隨時查看是否有新的 client 試圖建立連接,

這樣, 如果 server 在任何一個地方阻塞了, 其他 client 發送的內容就會受到

影響, 新 client 試圖建立連接也會受到影響. 因此, 我們使用 fcntl 將該

文件描述符變為非阻塞的:

fcntl( sockfd, F_SETFL, O_NONBLOCK);

// sockfd 是要改變狀態的文件描述符.

// F_SETFL 表明要改變文件描述符的狀態

// O_NONBLOCK 表示將文件描述符變為非阻塞的.

使用自然語言描述聊天室 server :

while ( 1) {

if 有新連接 then 建立並記錄該新連接;

for ( 所有的有效連接) {

if 該連接中有字符可讀 then {

for ( 所有其他的有效連接) {

將該字符串發送給該連接;

}

}

}

}

由於判斷是否有新連接, 是否可讀都是非阻塞的, 因此每次判斷, 不管

有還是沒有, 都會馬上返回. 這樣, 任何一個 client 向 server 發送字符

或者試圖建立新連接, 都不會對其他 client 的活動造成影響.

對 client 而言, 建立連接之後, 只需要處理兩個文件描述符, 一個是

建立了連接的 socket 描述符, 另一個是標准輸入. 和 server 一樣, 如果

使用阻塞方式的話, 很容易因為其中一個暫時沒有輸入而影響另外一個的讀

入. 因此將它們都變成非阻塞的, 然後client 進行如下動作:

while ( 不想退出) {

if ( 與 server 的連接有字符可讀) {

從該連接讀入, 並輸出到標准輸出上去.

}

if ( 標准輸入可讀) {

從標准輸入讀入, 並輸出到與 server 的連接中去.

}

}

 

上面的讀寫分別調用這樣兩個函數:

read( userfd[i], line, MAX_LINE);

// userfd[i] 是指第 i 個 client 連接的文件描述符.

// line 是指讀出的字符存放的位置.

// MAX_LINE 是一次最多讀出的字符數.

// 返回值是實際讀出的字符數.

write( userfd[j], line, strlen( line));

// userfd[j] 是第 j 個 client 的文件描述符.

// line 是要發送的字符串.

// strlen( line) 是要發送的字符串長度.

分析上面的程序可以知道, 不管是 server 還是 client, 它們都

不停的輪流查詢各個文件描述符, 一旦可讀就讀入並進行處理. 這樣的

程序, 不停的在執行, 只要有CPU 資源, 就不會放過. 因此對系統資源

的消耗非常大. server 或者 client 單獨執行時, CPU 資源的 98% 左

右都被其占用. 因此, 我們可以使用另外一種阻塞的方法來解決這個問題,

這就是 select.

2. select 方法

select 方法中, 所有文件描述符都是阻塞的. 使用 select 判斷一

組文件描述符中是否有一個可讀(寫), 如果沒有就阻塞, 直到有一個的

時候就被喚醒. 我們先看比較簡單的 client 的實現:

由於 client 只需要處理兩個文件描述符, 因此, 需要判斷是否有可

讀寫的文件描述符只需要加入兩項:

FD_ZERO( sockset);

// 將 sockset 清空

FD_SET( sockfd, sockset);

// 把 sockfd 加入到 sockset 集合中

FD_SET( 0, sockset);

// 把 0 (標准輸入) 加入到 sockset 集合中

然後 client 的處理如下:

while ( 不想退出) {

select( sockfd+1, &sockset, NULL, NULL, NULL);

// 此時該函數將阻塞直到標准輸入或者 sockfd 中有一個可讀為止

// 第一個參數是 0 和 sockfd 中的最大值加一

// 第二個參數是 讀集, 也就是 sockset

// 第三, 四個參數是寫集和異常集, 在本程序中都為空

// 第五個參數是超時時間, 即在指定時間內仍沒有可讀, 則出錯

// 並返回. 當這個參數為NULL 時, 超時時間被設置為無限長.

// 當 select 因為可讀返回時, sockset 中包含的只是可讀的

// 那些文件描述符.

if ( FD_ISSET( sockfd, &sockset)) {

// FD_ISSET 這個宏判斷 sockfd 是否屬於可讀的文件描述符

從 sockfd 中讀入, 輸出到標准輸出上去.

}

if ( FD_ISSET( 0, &sockset)) {

// FD_ISSET 這個宏判斷 sockfd 是否屬於可讀的文件描述符

從標准輸入讀入, 輸出到 sockfd 中去.

}

重新設置 sockset. (即將 sockset 清空, 並將 sockfd 和 0 加入)

}

下面看 server 的情況:

設置 sockset 如下:

FD_ZERO( sockset);

FD_SET( sockfd, sockset);

for ( 所有有效連接)

FD_SET( userfd[i], sockset);

}

maxfd = 最大的文件描述符號 + 1;

server 處理如下:

while ( 1) {

select( maxfd, &sockset, NULL, NULL, NULL);

if ( FD_ISSET( sockfd, &sockset)) {

// 有新連接

建立新連接, 並將該連接描述符加入到 sockset 中去了.

}

for ( 所有有效連接) {

if ( FD_ISSET ( userfd[i], &sockset)) {

// 該連接中有字符可讀

從該連接中讀入字符, 並發送到其他有效連接中去.

}

}

重新設置 sockset;

}

由於采用 select 機制, 因此當沒有字符可讀時, 程序處於阻塞狀態,

最小程度的占用CPU 資源, 在同一台機器上執行一個 server 和若干個

client 時, 系統負載只有 0.1 左右, 而采用原來的 fcntl 方法, 只運行

一個 server, 系統負載就可以達到 1.5 左右. 因此我們推薦使用 select.

*****************************************************************

By Simon Lei, Jul.01,1999. All Rights Reserved.

歡迎傳播, 請保留作者信息.


摘自:http://falsy.myrice.com/linux.html


Copyright © Linux教程網 All Rights Reserved