1. 程式人生 > >基於TCP的伺服器端/客戶端(二)---------網路程式設計(Linux----C)

基於TCP的伺服器端/客戶端(二)---------網路程式設計(Linux----C)

基於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緩衝在建立套接字時自動生成。
  • 即使關閉套接字也會繼續傳遞輸出緩衝中遺留的資料。
  • 關閉套接字將丟失輸入緩衝中的資料。
(2)TCP內部工作原理1:與對方套接字連線
TCP套接字從建立到消失所經過過程分為3步:
  • 與對方套接字建立連線。
  • 與對方套接字進行資料交換。
  • 斷開與對方套接字的連線。
TCP在實際通訊過程中也會經過3次對話過程,因此,該過程又稱Three-way handshaking(三次握手)
套接字是以全雙工方式工作的。也就是,它可以雙向傳遞資料。
(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訊息後未接收資料而重傳的。