基於TCP的伺服器端/客戶端(二)---------網路程式設計(Linux----C)
阿新 • • 發佈:2019-02-13
基於TCP的伺服器端/客戶端(二)---網路程式設計(Linux--C)
在基於TCP的伺服器端/客戶端(一)中的回聲客戶端存在的問題:
下列是echo_client.c中的程式碼:
write(sock,message,strlen(message));
str_len=read(sock,message,1024-1); message[str_len]=0; printf("Message from server:%s",message);
以上程式碼有個錯誤假設:每次呼叫read、write函式時都會以字串為單位執行實際的I/O操作。
上述客戶端是基於TCP的,多次呼叫write函式傳遞的字串有可能一次性傳遞到伺服器端。此時客戶端有可能從伺服器
端收到多個字串,這不是我們希望看到的結果。
1、回聲伺服器端沒有問題,只有回聲客戶端有問題
在echo_server.c的第50-51行程式碼:
while((str_len=read(clnt_sock,messag,1024))!=0)
write(clnt_sock,messag,str_len);
在echo_client.c的第45-46行程式碼:
write(sock,message,strlen(message));
str_len=read(sock,message,1024-1);
兩者都在迴圈呼叫read或write函式。實際上之前的回聲客戶端將100%接收自己傳輸的資料,只不過接收資料時的單位有問題。
下面是echo_client.c第37行開始的程式碼:
while(1)
{ fputs("Input message(Q to quit):",stdout); fgets(message,1024,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);}
回聲客戶端傳輸的是字串,而且是通過呼叫write函式一次性發送的。之後還呼叫一次read函式,期待著接收自己傳輸的字串,這就是問題所在。
(1)回聲客戶端問題解決方法可以提前確定接收資料的大小。若之前輸出了20位元組長的字串,則在接收時迴圈呼叫read函式讀取20個位元組即可。echo_client2.c程式碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len,recv_len,recv_cnt;
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(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect()error");
else
puts("Connected..........");
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n")||!strcmp(message,"Q\n"))
break;
str_len=write(sock,message,strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock,&message[recv_len],BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read()error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
printf("Message from server:%s",message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
(2)如果問題不在於回聲客戶端:定義應用層協議
回聲客戶端可以提前知道接收的資料長度,但更多情況下這不太可能。
若無法預知接收資料的長度時,此時需要就是應用層協議的定義。
之前的回聲伺服器端/客戶端中定義瞭如下協議:“收到Q就立即終止連線”。
同樣,收發資料過程中也需要定好規則(協議)以表示資料的邊界,或提前告知收發資料的大小。伺服器端/客戶端
實現過程中逐步定義的這些規則集合就是應用層協議。
客戶端op_client.c程式碼:
#include <stdio.h>
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 #define RLT_SIZE 4 #define OPSZ 4 void error_handling(char *message); int main(int argc,char *argv[]) { int sock; char opmsg[BUF_SIZE]; int result,opnd_cnt,i; 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(argv[1]); serv_adr.sin_port=htons(atoi(argv[2])); if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1) error_handling("connect() error!"); else puts("Connected......"); fputs("Operand count:",stdout); scanf("%d",&opnd_cnt); opmsg[0]=(char)opnd_cnt; for(i=0;i<opnd_cnt;i++) { printf("Operand %d:",i+1); scanf("%d",(int*)&opmsg[i*OPSZ+1]); } fgetc(stdin); fputs("Operator:",stdout); scanf("%c",&opmsg[opnd_cnt*OPSZ+1]); write(sock,opmsg,opnd_cnt*OPSZ+2); read(sock,&result,RLT_SIZE); printf("Operation result:%d\n",result); close(sock); return 0; } void error_handling(char *message) { fputs(message,stderr); fputc('\n',stderr); exit(1); }
伺服器端op_server.c程式碼:
#include <stdio.h>
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 #define OPSZ 4 void error_handling(char *message); int calculate(int opnum,int opnds[],char operator);int main(int argc,char *argv[]) { int serv_sock,clnt_sock; char opinfo[BUF_SIZE]; int result,opnd_cnt,i; int recv_cnt,recv_len; struct sockaddr_in serv_adr,clnt_adr; socklen_t clnt_adr_sz; if(argc!=2) { printf("Usage:%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++) { opnd_cnt=0; clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz); read(clnt_sock,&opnd_cnt,1); recv_len=0; while((opnd_cnt*OPSZ+1)>recv_len) { recv_cnt=read(clnt_sock,&opinfo[recv_len],BUF_SIZE-1); recv_len+=recv_cnt; } result=calculate(opnd_cnt,(int*)opinfo,opinfo[recv_len-1]); write(clnt_sock,(char*)&result,sizeof(result)); close(clnt_sock); } close(serv_sock); return 0; } int calculate(int opnum,int opnds[],char op) { int result=opnds[0],i; switch(op) { case '+': for(i=1;i<opnum;i++) result+=opnds[i]; break; case '-': for(i=1;i<opnum;i++) result-=opnds[i]; break; case '*': for(i=1;i<opnum;i++) result*=opnds[i]; break; } return result; } void error_handling(char *message) { fputs(message,stderr); fputc('\n',stderr); exit(1); }
編譯與執行
2、TCP原理
(1)TCP套接字中的I/O緩衝
TCP套接字的資料收發無邊界。伺服器端即使呼叫1次write函式傳輸40位元組的資料,客戶端也有可能通過4次read函式
呼叫每次讀取10位元組。
緩衝特性整理:
- I/O緩衝在每個TCP套接字中單獨存在。
- I/O緩衝在建立套接字時自動生成。
- 即使關閉套接字也會繼續傳遞輸出緩衝中遺留的資料。
- 關閉套接字將丟失輸入緩衝中的資料。
TCP套接字從建立到消失所經過過程分為3步:
- 與對方套接字建立連線。
- 與對方套接字進行資料交換。
- 斷開與對方套接字的連線。
套接字是以全雙工方式工作的。也就是,它可以雙向傳遞資料。
(3)TCP內部工作原理2:與對方主機的資料交換
主機A分2次(分2個數據包)向主機B傳遞200位元組的過程。首先,主機A通過1個數據包傳送100個位元組的資料,資料包的SEQ為1200。
主機B為了確認這一點,向主機A傳送ACK1301訊息。此時的ACK號為1301而非1201,原因在於ACK號的增量為傳輸的資料位元組數。假設
每次ACK號不加傳輸的位元組數,這樣雖然可以確認資料包的傳輸,但無法明確100位元組全都正確傳遞還是丟失一部分,比如只傳遞
了80位元組。因此按如下公式傳遞ACK訊息:
ACK號→SEQ號+傳遞的位元組數+1
下面傳輸過程中資料包消失的情況
(4)TCP內部工作原理3:斷開與套接字的連線
先由套接字A向套接字B傳遞斷開連線的訊息,套接字B發出確認收到的訊息,然後向套接字A傳遞可以斷開連線的訊息,套接字A同樣確認資訊。
資料包內的FIN表示斷開連線。即雙方各發送1次FIN訊息後斷開連線。此過程經過4個階段,因此又稱四次握手(Four-way handshaking)。
如圖向主機A傳遞了兩次ACK 5001,其實,第二次FIN資料包中的ACK 5001只是因為接收ACK訊息後未接收資料而重傳的。