實現(xiàn)基于TCP的服務器端
#include <sys/socket.h>
/*
* @params
* sock: 服務器套接字文件描述符。
* backlog: 等待連接的請求的隊列長度
*/
int listen(int sock, int backlog); // 0: 成功。 -1: 失敗。
#include <sys/socket.h>
/*
* @params
* sock: 服務器套接字文件描述符。這里的套接字只是為了接收連接的,因為連接本身也是一種數(shù)據(jù),需要用套接字來接收。
* addr: 客戶端地址。傳入時是空值,接收到連接后填入該客戶端的地址信息。
* addrlen: 第二個參數(shù)addr結(jié)構(gòu)的長度。在調(diào)用后填入。
*/
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); // 成功返回新創(chuàng)建的套接字文件描述符,失敗返回-1。
實現(xiàn)基于TCP的客戶端
#include <sys/socket.h>
/*
* @params
* sock: 客戶端套接字文件描述符。這里的套接字只是為了接收連接的,因為連接本身也是一種數(shù)據(jù),需要用套接字來接收。
* addr: 目標服務器地址。
* addrlen: 第二個參數(shù)addr結(jié)構(gòu)的長度。
*/
int connect(int sock, struct sockaddr *addr, socklen_t addrlen); // 0: 成功。 -1: 失敗。
客戶端的IP地址就是主機的IP地址,端口在調(diào)用connect()時自動由內(nèi)核隨機分配。
發(fā)生以下兩種情況之一就會返回:
- 服務器端將請求記錄到等待隊列。
- 發(fā)生斷網(wǎng)等異常情況而中斷請求。
實現(xiàn)迭代服務器/客戶端
# ./eserver 9190
Connected client 1
Connected client 2
Connected client 3
Connected client 4
Connected client 5
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): orange
Message from server: orange
Input message (Q to quit): bottle
Message from server: bottle
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): when
Message from server: when
Input message (Q to quit): where
Message from server: where
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): FIRST
Message from server: FIRST
Input message (Q to quit): Q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): sunny
Message from server: sunny
Input message (Q to quit): smile
Message from server: smile
Input message (Q to quit): q
# ./eclient 127.0.0.1 9190
Connected
Input message (Q to quit): last one
Message from server: last one
Input message (Q to quit): q
這只是一個簡單的例子而已。實際上客戶端服務器的處理不是很謹慎,如果他們不在同一臺機器上,發(fā)生了網(wǎng)絡延遲等狀況,可能會粘包或分包。
習題
- 請說明TCP/IP的4層協(xié)議棧,并說明TCP和UDP套接字經(jīng)過的層級結(jié)構(gòu)差異。
從低到高依次是數(shù)據(jù)鏈路層、網(wǎng)絡層、傳輸層和應用層。TCP和UDP的差異在傳輸層。- 請說出TCP/IP協(xié)議棧中鏈路層和IP層的作用,并給出兩者關系。
鏈路層提供物理連接,IP層基于物理鏈路選擇合適的路徑。- 為何需要把TCP/IP協(xié)議棧分成4層(或7層)?結(jié)合開放式系統(tǒng)回答。
為了通過標準化操作設計開放式系統(tǒng)。- 客戶端調(diào)用connect函數(shù)向服務器端發(fā)送連接請求。服務器端調(diào)用哪個函數(shù)后,客戶端可以調(diào)用connect函數(shù)?
必須在服務器端調(diào)用listen函數(shù)后。- 什么時候創(chuàng)建連接請求等待隊列?它有何作用?與accept有什么關系?
listen函數(shù)創(chuàng)建連接請求等待隊列。使得同時只能處理一個客戶端連接的服務器暫存其他客戶端的連接,以待后續(xù)處理。accept從隊列中取第一個進行服務。- 客戶端中為何不需要調(diào)用bind函數(shù)分配地址?如果不調(diào)用bind函數(shù),那何時、如何向套接字分配IP地址和端口號?
因為在這里客戶端是主動發(fā)起連接的一端,它不需要在某個固定的地址和端口去監(jiān)聽連接。在調(diào)用connect的時候內(nèi)核會自動隨機分配地址和端口,服務器可以根據(jù)收到的消息解析出客戶端的地址和端口。- 把第1章的hello_server.c改成迭代服務器段,并利用客戶端測試更改是否準確。
同本章例子。
我的問題
- 服務器端是阻塞在listen()還是accept()?
阻塞在accept()。listen()的作用只是使主動連接套接字變?yōu)楸贿B接套接字,使得一個進程可以接受其它進程的請求。因此客戶端的connect()可以發(fā)生在listen()的前面或后面。如果在前面則進入等待隊列,如果在后面則直接被服務器接受請求。 - 為什么循環(huán)接收同一個client的數(shù)據(jù),判斷read()返回值用0?
當接收隊列為空,且本端或?qū)Χ苏{(diào)用shutdown或close連接,read()才會返回零。并不是接收隊列為空就返回0。所以該函數(shù)能一直讀到client退出輸入循環(huán)。
附錄
[1] 探討read的返回值的三種情況
[2] Github