基於TCP的服務端/客戶端
基於TCP的服務端/客戶端
根據資料傳輸方式不同,基於網路協議的套接字一般分為TCP套接字(也叫基於流stream的套接字)和UDP套接字。
TCP/IP協議棧(Stack,層)eg:
應用層
/ \
TCP UDP
\ /
IP層
|
鏈路層
各層可能通過作業系統等軟體實現,也可能通過類似NIC的硬體裝置實現
0>開放系統
以多個標準為依據設計的系統稱為開放系統,TCP/IP協議棧也屬於其中之一
優點:路由器來完成IP層互動任務,網絡卡:網絡卡製造商都會遵守鏈路層的協議標準
1>鏈路層:是物理連結領或標準的結果,也是最基本的領域,專門定義LAN,WAN,MAN等網路標準
2>IP層:準備好物理連線後就要傳輸資料。為了在複雜的網路中傳輸資料,首先需要考慮路徑的選擇,向目標傳輸
資料需要經過哪條路徑?解決此問題就是IP層,該層使用的協議就是IP。IP本身是面向訊息的,不可靠的協議。每次
傳輸資料時會幫我們選擇路徑,但並不一致。如果傳輸中發生路徑錯誤,則選擇其他路徑;但如果發生資料丟失或
錯誤,則無法解決--IP協議無法應對資料錯誤
3>TCP/UDP層:IP層解決資料傳輸中的路徑選擇問題,只需照此路徑傳資料即可。TCP和UDP層以
基礎完成實際資料傳輸--傳輸層。UDP比TCP簡單
TCP與IP層二者關係:
IP層只關注1個數據包(資料傳輸的基本單位)的傳輸過程。因此,即使傳輸多個數據包,每個資料包也是由IP層實際傳輸的
,也就是說傳輸本身是不可靠的。若只利用IP層傳輸資料,則有可能導致後傳輸的資料包B比先傳輸的資料包A提早到達。另外
,傳輸的資料包A/B/C中有可能收到A和C,甚至收到的C可能已損毀
TCP如果資料交換過程中可以確認對方已收到資料,並重傳丟失的資料,那麼即便IP層不保證資料傳輸,這類通訊也是可靠的
4>應用層:
上述內容是套接字通訊過程中自動處理的。選擇資料傳輸路徑,資料確認過程都被隱藏到套接字內部。而與其說是
如“使程式設計師從這些細節中解放出來”的表達更為準確。程式設計師程式設計時無需考慮這些過程,但這並不意味著不用掌握這些短識。只有
掌握了這些理論,才能編寫出符合需求的網路程式
總之,向各位提供的工具就是套接字,只需利用套接字編出程式即可。編寫軟體的過程中,需要根據程式特點決定伺服器和客戶端之
間的資料傳輸規則---應用層協議。
網路程式設計的大部份內容就是設計並實現應用層協議
8.1 實現基於TCP的伺服器端/客戶端
TCP的伺服器端預設的函式呼叫順序:
socket() 建立套接字
|
V
bind()分配套接字地址
|
V
listen()等待連線請求狀態
|
V
accept() 充許連線
|
V
read()/write() 資料交換
|
V
close() 斷開連線
socket,bind前面已說明,bind()(給套接字分配了地址,接下來就要通過呼叫listen()進入等待連線請求狀態
只有呼叫了listen(),客戶端才能進入可發出連線請求的狀態----這時客戶端才能呼叫connect()(若提前呼叫將發生錯誤)
int listen(int sock,int backlog);// 成功時返回0,失敗時返回-1
sock:希望進入等待連線狀態的套接字檔案描述符,傳遞的描述符套接字引數為服務端套接字(監聽套接字)
backlog:連線請求等待佇列(Queue)的長度,若為5,則佇列長度為5,表示最多使5個連線請求進入佇列。(與服務端的特性有關,
像頻繁接收請求的Web服務端至少應為15.另外,連線請求佇列的大小始終根據實驗結果而定。)
int accept(int sock,struct sockaddr *addr,socklen_t *addrlen);//成功返回建立的套接字檔案描述符,失敗返回-1;
sock:伺服器套接字的檔案描述符
addr:儲存發起連線請求的客戶端地址資訊的變數地址值,呼叫函式後向傳遞來的地址變數引數填充客房端地址資訊
addrlen:第二個引數addr結構體的長度,但時存有長度的變數地址。函式呼叫完成後,該變數即被填入客戶端地址長度
accept()受理連線請求等待佇列中待處理的客戶端連線請求。函式呼叫成功時,accetp()內部將產生用於資料I/O的套接字,並返回其
檔案描述符。需要強調的是,套接字是自動建立的,並自動與發起連線請求的客戶端建立連線。
呼叫accept()從隊頭取1個連線請求與客戶端建立連線,並返回建立的套接字檔案描述符。另外,呼叫accetp()時若等待佇列為空,則
accept()不會返回,直到佇列中出現新的客戶端連線
8.2 TCP客戶端的預設函式呼叫順序
socket() 建立套接字
|
V
connect()請求連線
|
V
read()/write() 資料交換
|
V
close() 斷開連線
與服務端相比,區別就在於"請求連線“,它是建立客戶端套接字後向伺服器端發起的連線請求。伺服器端呼叫
listen()建立連線請求等待佇列,之後客戶端即可請求連線。
int connect(int sock, struct sockaddr *servaddr, socklen_t addrlen);//成功時返回0,失敗時返回-1;
sock:客戶端套接字檔案描述符
servaddr:儲存目標伺服器端地址資訊的變數地址值
addrlen:以位元組為單位傳遞已傳遞給第二個結構體引數servaddr的地址變數找度
客戶端呼叫connect(),發生以下情況之一才會返回(完成函式呼叫)
1>伺服器端接收連線請求
2>發生斷網等異常情況而中斷連線請求
需要注意,所謂的“接收連線”並不意味著伺服器端呼叫accept(),其實是服務端把連線請求資訊記錄到等待佇列。因此connect()
返回後並不立即進行資料交換
3>實現服務端必經過過程之一就是給套接字分配IP和埠號。但客戶實現過程中並未出現套接字地址分配,而是建立套接字後立即呼叫
connect(). 客戶端呼叫connect()時,作業系統,更準確地說是在核心中,IP用計算機(主機)的IP,埠隨機分配地址---客戶端
的IP和埠呼叫connect()時自動分配,無需呼叫標記的bind()進行分配
8.3 基於TCP的服務端/客戶端函式呼叫關係
前面講解TCP伺服器/客戶端的實現順序,實際上二者並非相互獨立
伺服器
socket() 建立套接字
|
V
bind()分配套接字地址客戶端
| socket() 建立套接字
V |
listen()等待連線請求狀態 V
| <---------------------------—--connect()請求連線
V or / |
accept() 充許連線 /
| <------------------------ / |
V V
read()/write() 資料交換 <-------> read()/write()資料交換
| |
V V
close() 斷開連線 <-------------> close()斷開連線
總體流程整理:伺服器端建立套接字後連續呼叫bind,listen函式進入等待狀態,客戶端通過呼叫connect()
發起連線請求.需要注意的是,客戶端只能等到服務端呼叫listen()後才能調connect().同時要清楚,客戶端呼叫
connect()前,伺服器端有可能率先呼叫accept().當然,此時服務端在呼叫accept()時進入阻塞(blocking)狀
態,直到客戶端呼叫connect()為止
8.4實現迭代伺服器端/客戶端
1>實現迭代伺服器端
編寫回聲(echo)服務端/客戶端----服務端將客戶端傳輸的字串資料原封不動地傳回客戶端,就像回聲一樣。
之前的hello world服務端處理完髮1個客戶端連線請求即退出,連線請求等待佇列實際沒有太大意義。
設定好等待佇列的大小後,應向所有客戶疫提供服務。如果想繼續受理後續的客戶疫連線請求,最簡單的辦法就是
插入迴圈語句反覆呼叫accetp()
socket() 建立套接字
|
V
bind()分配套接字地址
|
V
listen()等待連線請求狀態
| <--------------------
V |
accept() 充許連線 |
| |
V |
read()/write() 資料交換 |
|---------------------
V
close() 斷開連線
呼叫accept()後,緊接著呼叫I/O相關的read(),write(),然後呼叫close(),這並非針對服務端套接字,而
是針對accept()呼叫時建立的套接字。呼叫close()就意味著結束了針對某一客戶端的服務,此時如果還想服務於
其他客戶端,就要重新呼叫accept()
2>實現迭代伺服器端/客戶端
前面講的就是迭代伺服器端。媽即使服務端以迭代方式運轉,客戶端程式碼亦無太大區別。
程式的基本執行方式:
.服務端在同一時刻只與一個客戶端相連,並提供回聲服務
.服務端依次向5個客戶端提供服務並退出
.客戶端接收使用者輸入的字串併發送到服務端
.服務端將接收到的字串資料傳回客戶端,即“回聲”
.服務端與客戶端之間的字串回聲一直執行到客戶端輸入Q為止
// 服務端
#define BUF_SIZE 1024
int echo_serverc(int argc,char *argv[]) {
int serv_sock,clnt_sock;
char message[BUF_SIZE];
int str_len,i;
struct sockaddr_in serv_adr,clnt_adr;
socklen_t clnt_adr_sz;
if (argc!=2) {
printf("usege:%s<port>\n",argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr= htonl(INADDR_ANY);
serv_adr.sin_port= htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr))== -1) {
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1) {
error_handling("listen() error");
}
clnt_adr_sz = sizeof(clnt_adr);
for ( i = 0; i < 5; i ++) {
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
if (clnt_sock == -1) {
error_handling("accept() error");
}else {
printf("Connected client %d \n",i + 1);
}
while ((str_len = (int)read(clnt_sock, message, BUF_SIZE))!=0) {
write(clnt_sock, message, str_len);
}
close(serv_sock);
}
return 0;
}
// 客戶端
#define BUF_SIZE 1024
int echo_clientc(int argc,char *argv[]) {
int sock;
char message[BUF_SIZE];
long str_len;
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage :%s <IP> <Port> \n",argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
error_handling("socket error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_adr.sin_port = htons(atoi("9190"));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr))== -1) {
error_handling("connect error");
}else {
puts("Conneted...");
}
while (1) {
fputs("input messae(Q to quit):", stdout);
fgets(message,BUF_SIZE,stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len]= 0;
printf("message from server:%s",message);
}
close(sock);
return 0;
}
...