基於TCP協議的伺服器/客戶端程式
阿新 • • 發佈:2019-01-22
作為傳輸層的主要協議,TCP協議不僅可以支援本地的資料通訊,還可以支援跨網路的程序間通訊。
在偌大的網際網路中,我們可以通過“IP地址+端⼜號”標識網際網路中唯一的一個程序。然而,“IP地址+端⼜號”就稱為socket,這就是網路socket程式設計。
在TCP協議中,建⽴連線的兩個程序各⾃有⼀個socket來標識,那麼這兩個socket組成 的socketpair就唯⼀標識⼀個連線。
socket本⾝有“插座”的意思,因此⽤來描述⽹絡連線的⼀ 對⼀關係。
要寫出一個基於TCP的網路服務程式,我們應該具有以下的知識:
- 傳送主機通常將傳送緩衝區中的資料按記憶體地址從低到⾼的順序發出,接收主機把從⽹絡上接到的位元組依次儲存在接收緩衝區中,也是按記憶體地址從低到⾼的順序儲存。
- TCP/IP協議規定,⽹絡資料流應採⽤⼤端位元組序,即低地址⾼位元組。
- socket API是⼀層抽象的⽹絡程式設計接⼜,適⽤於各種底層⽹絡協議,如IPv4、 IPv6,以及後⾯要講的UNIX Domain Socket。
通常⽤點分⼗進位制的字串表⽰IP 地址,以下函式可以在字串表⽰ 和in_addr表⽰之間轉換。
字串轉in_addr的函式:
in_addr轉字串的函式:
以下是一次客戶端,服務端從請求連線到斷開連線的依次完整會話過程:
伺服器調⽤socket()、 bind()、 listen() 完成初始化後,調⽤accept()阻塞等待,處於監聽端⼜的狀態,客戶端調⽤socket()初始化後,調⽤connect()發出SYN段並阻塞等待伺服器應答,伺服器應答⼀個SYN-ACK段,客戶端收到後從connect()返回,同時應答⼀個ACK段,伺服器收到後 從accept()返回。
有了上面的分析過程,不難寫出一下程式: (一)server.c 的作⽤是接受client的請求,並與client進⾏簡單的資料通訊,整體為⼀個阻塞式的⽹絡聊天⼯具。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
// exit(1);
return 4;
}
int listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0)
{
perror("socket");
return 1;
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 2;
}
listen(listen_sock,5);//max listen bind 5
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int fd=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(fd<0)
{
perror("accept");
return 3;
}
char buf[1024];
while(1)
{
memset(buf,'\0',sizeof(buf));
ssize_t _s=read(fd,buf,sizeof(buf)-1);
if(_s>0)
{
buf[_s]='\0';
printf("client :");
printf("%s\n",buf);
write(fd,buf,strlen(buf));
printf("server :%s\n",buf);
}else
{
printf("read done....\n");
break;
}
}
return 0;
}
</span></span>
socket()開啟⼀個⽹絡通訊端⼜,如果成功的話,就像open()⼀樣返回⼀個⽂件描述符,應⽤程式可以像讀寫⽂件⼀樣⽤read/write在⽹絡上收發資料,如果socket()調⽤出錯則返回-1。對 於IPv4,family引數指定為AF_INET。對於TCP協議,type引數指定為SOCK_STREAM,表⽰⾯向流的傳輸協議。如果是UDP協議,則type引數指定為SOCK_DGRAM,表⽰⾯向資料報的傳輸協
議。 protocol引數的介紹從略,指定為0即可。bind()的作⽤是將引數sockfd和myaddr繫結在⼀起,使sockfd這個⽤於⽹絡通訊的⽂件描述符監聽myaddr所描述的地址和端⼜號。
listen()宣告sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連線等待狀態,如果接收到更多的連線請求就忽略。
三⽅握⼿完成後,伺服器調⽤accept()接受連線,如果伺服器調⽤accept()時還沒有客戶端的連線請 求,就阻塞等待直到有客戶端連線上來。 cliaddr是⼀個傳出引數,accept()返回時傳出客戶端的地 址和端⼜號。 addrlen引數是⼀個傳⼊傳出引數(value-result argument),傳⼊的是調⽤者提供的 緩衝區cliaddr 的長度以避免緩衝區溢位問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調⽤者提供的緩衝區)。如果給cliaddr 引數傳NULL,表⽰不關⼼客戶端的地址。
(二)client.c的作⽤是連結server,並向server發起通訊請求。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
// exit(1);
return 4;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 1;
}
struct sockaddr_in remote;
remote.sin_family=AF_INET;
remote.sin_port=htons(atoi(argv[2]));
remote.sin_addr.s_addr=inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&remote,sizeof(remote))<0)
{
perror("connect");
return 2;
}
while(1)
{
char buf[1024];
memset(buf,'\0',sizeof(buf));
printf("client :");
fflush(stdout);
ssize_t _s=read(0,buf,sizeof(buf)-1);
if(_s>0)
{
buf[_s-1]='\0';
write(sock,buf,strlen(buf));
memset(buf,'\0',sizeof(buf));
_s=read(sock,buf,sizeof(buf));
printf("server :");
printf("%s\n",buf);
}else
{
printf("read done....\n");
break;
}
}
return 0;
}
</span></span>
由於客戶端不需要固定的端⼜號,因此不必調⽤bind(),客戶端的端⼜號由核心⾃動分配。注意, 客戶端不是不允許調⽤bind(),只是沒有必要調⽤bind()固定⼀個端⼜號,伺服器也不是必須調⽤bind(),但如果伺服器不調⽤bind(),核心會⾃動給伺服器分配監聽端⼜,每次啟動伺服器時端⼜ 號都不⼀樣,客戶端要連線伺服器就會遇到⿇煩。
客戶端需要調⽤connect()連線伺服器,connect和bind的引數形式⼀致,區別在於bind的引數是⾃⼰的地址,⽽connect的引數是對⽅的地址。 connect()成功返回0,出錯返回-1。我們來看一下server和client是怎樣通訊的(執行截圖):
(一)先執行伺服器:
伺服器阻塞等待客戶機請求連結。 (二)重啟一個終端,然後執行客戶機:
(三)然後客戶機發送資料:
(四)伺服器接到資料,並回顯給客戶機:
這裡有一個問題,當我們ctrl+Z暫停伺服器,然後再起一個服務時,它會提醒我們Address already in use。這就像伺服器掛了
client終⽌時⾃動關閉socket描述符,server的TCP連線收到client發的FIN段後處於TIME_WAIT狀 態。 TCP協議規定,主動關閉連線的⼀⽅要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間後才能回到CLOSED狀態,因為我們先Ctrl-C終⽌了server,所以server是主動關閉連線的⼀⽅,在TIME_WAIT期間仍然不能再次監聽同樣的server端 ⼜。 MSL在RFC1122中規定為兩分鐘,但是各作業系統的實現不同,在Linux上⼀般經過半分鐘後 就可以再次啟動server了。 解決方法是:在socket呼叫和bind呼叫之間加上一段對socket的設定:
int opt = 1;
setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))