【網路】實現簡單的TCP、UDP伺服器、TCP多程序/多執行緒伺服器
阿新 • • 發佈:2019-02-09
1.0 一個簡單的TCP伺服器(只服務一個客戶端)
先看程式碼如下:
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n" ,proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//建立套接字
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if (bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//繫結套接字
perror("bind");
exit(3);
}
if(listen(sock,5) < 0){//監聽套接字
perror("listen");
exit(4);
}
return sock;//將最終得到的監聽套接字返回
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0 ]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//獲得監聽套接字
while(1)
{
struct sockaddr_in client;//用來獲得傳送者的資訊(ip地址和埠號)
socklen_t len = sizeof(client);
//建立連線,得到新套接字進行操作
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
printf("get a connect,ip is %s,port is %u\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
while(1)
{//先讀再寫
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//讀成功
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//讀到的位元組數為0,表明對端結束已經關閉
printf("client exit\n");
break;
}
else{//讀失敗
perror("read");
break;
}
}
close(new_sock);
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//建立套接字
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//請求與伺服器建立TCP連線
perror("connect");
return 3;
}
while(1)
{//先寫在讀
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//從標準輸入(鍵盤)中讀資料到buf
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//實現伺服器的回顯
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
return 0;
}
····第一次連線的現象
client.c
server.c
·····client.c結束後的現象
client.c
server.c
····再次重新連線後的現象
client.c
server.c
但是有個問題,上面實現的簡單伺服器,每次只能連線一個客戶端,當有多個客戶端同時請求與伺服器連線時,伺服器只能處理一個的請求與一個客戶端相連,剩下的連線請求先阻塞在連線佇列,當伺服器處理完某個客戶端的連線,再從佇列裡拿一個連線進行處理。如下。
37161埠
37162埠
伺服器處理完37161,才處理37162,因為伺服器在處理37161的時候,37162給伺服器發訊息,伺服器不作迴應。
但是隻能進行一對一的連線,與我們平常所見到的伺服器功能不符,單程序的伺服器畢竟來說可服務的物件就很小,所以現在實現一個多程序的伺服器。那麼接下來我們來實現一個一對多的伺服器。
2.0 實現TCP多程序伺服器
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
if(listen(sock,10) < 0){//listen
perror("listen");
exit(4);
}
return sock;//return an "listen_sock";
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//get listen_sock
while(1)
{
struct sockaddr_in client;//get sender's information
socklen_t len = sizeof(client);
//get real socket that we will operator
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
printf("get a connect,ip is %s,port is %u\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pid_t id = fork();
if(id < 0){
perror("fork");
close(new_sock);
}
else if(id == 0)
{//child
close(listen_sock);
if(fork() > 0){//again fork , create sun zi process and child process end
exit(0);
}
//sun zi process working
while(1)
{//first read,then write
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//read successful
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//read to file's end
printf("client exit\n");
break;
}
else{//read fail
perror("read");
break;
}
}
close(new_sock);
exit(0);
}
else
{//father
close(new_sock);
waitpid(id,NULL,0);
}
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//connect
perror("connect");
return 3;
}
while(1)
{//first write,then read
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//read data from stdin
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//shi xian server hui xian
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
在server.c中,我們先fock了兩個程序父程序和子程序。父程序的工作是不斷地返回accept處拿new_sock,子程序的工作是不斷地去處理new_sock。也就是說父程序只管拿,子程序只管處理。所以父程序關閉不需要的套接字new_sock,而只留下listen_sock ; 子程序關閉不需要的listen_sock,而留下new_sock.
細心地你也許會發現,可是為什麼還在子程序中再次fock建立一個孫子程序?這也正是這段程式碼的巧妙之處。
通過之前的學習我們知道,子程序退出後變成殭屍程序,直到父程序呼叫wait或者waitpid來對子程序資源進行回收。所以在第一次fork完成後,我們父程序會呼叫waitpid等待函式去等待子程序結束,但是它目前是阻塞式等待。為了解決阻塞問題,我們在子程序中又建立了一個孫子程序,並且讓子程序退出,讓孫子程序去完成子程序的工作。而對於孫子程序來說,子程序的退出使孫子程序變成孤兒程序,被1號程序Init收養,若孫子程序退出也是由Init程序回收它,與父程序中的waitpid無關。這樣當子程序退出時,父程序的waitpid對子程序資源進行了回收,並且回收後因為子程序已經不存在了,waitpid也失去了它的作用,就不會阻塞了。這就是這段程式碼的巧妙之處。
結果如圖:
client1
client2
server
3.0 實現TCP多執行緒伺服器
基於多執行緒的伺服器和多程序的差不多。程式碼如下。
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int StartUp(const char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;//IPV4
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
if(listen(sock,10) < 0){//listen
perror("listen");
exit(4);
}
return sock;//return an "listen_sock";
}
void* request(void* arg)
{
int new_sock = (int)arg;
printf("get a new client\n");
while(1)
{//first read,then write
char buf[1024];
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0){//read successful
buf[s] = '\0';
printf("client#:%s\n",buf);
write(new_sock,buf,strlen(buf));//server hui xian
}
else if(s == 0){//read to file's end
printf("client exit\n");
break;
}
else{//read fail
perror("read");
break;
}
}
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int listen_sock = StartUp(argv[1],atoi(argv[2]));//get listen_sock
while(1)
{
struct sockaddr_in client;//get sender's information
socklen_t len = sizeof(client);
//get real socket that we will operator
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0){
perror("accept");//if fail,again listen
continue;
}
pthread_t id;
pthread_create(&id,NULL,&request,(void*)new_sock);
pthread_detach(id);
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//connect
perror("connect");
return 3;
}
while(1)
{//first write,then read
printf("client#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);//read data from stdin
if(s > 0){
buf[s-1] = '\0';
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);//shi xian server hui xian
if(_s > 0){
buf[_s] = '\0';
printf("server echo#%s\n",buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
thread1
thread2
server
4.0 實現UDP伺服器
UDP伺服器進行讀寫時,用的不是read和write,而是以下兩個函式。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
//均返回:若成功則為讀或寫的位元組數,出錯為-1
前三個引數:sockfd, buff, nbytes等同於read和write的前三個引數:描述字,指向讀入或者寫出緩衝區的指標,讀寫位元組數。
函式sendto的引數to是一個含有資料將發往的協議地址(例如IP地址和埠號)的套介面地址結構,它的大小由addrlen來指定。函式recvfrom用資料報傳送者的協議地址裝填由from所指的套介面地址結構,儲存在此套介面地址結構中的位元組數也以addrlen所指的整數返回給呼叫者。注意,sendto的最後一個引數是一個整數值,而recvfrom的最後一個引數值是一個指向整數值的指標。
recvfrom的最後兩個引數類似於accept的最後兩個引數:返回時套介面地址結構的內容告訴我們是誰傳送了資料報(UDP情況下)或是誰發起了連線(TCP情況下)。sendto的最後兩個引數類似於connect的最後兩個引數:我們用資料報將發往(UDP情況下)或與之建立連線(TCP情況下)的協議地址來裝填套介面地址結構。
寫一個長度為0的資料報是可行的,這也意味著對於資料報協議,recvfrom返回0值也是可行的;它不表示對方已經關閉了連線,這與TCP套介面上的read返回0的情況不同。由於UDP伺服器也是伺服器,所以需要繫結,但因為UDP是無連線的,所以不需要監聽和連線,這就沒有諸如關閉UDP連線之類的事情。
PS:預設情況recvfrom函式沒有接收到對方資料時候是阻塞的
實現程式碼如下:
server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
static void Usage(const char* proc)
{
printf("Usage:%s[local_ip][local_port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);//create socket
if(sock < 0){
perror("socket");
exit(2);
}
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(sock,(struct sockaddr*)&local,sizeof(local)) < 0){//bind
perror("bind");
exit(3);
}
printf("get a client\n");
while(1)
{//先讀再寫
printf("[%s:%d]\n",inet_ntoa(local.sin_addr),ntohs(local.sin_port));
struct sockaddr_in client;//獲得傳送方的信心(ip地址和埠號)
socklen_t len = sizeof(client);
char buf[1024];
int s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);//與accept相似
if(s < 0){
perror("recvfrom");
continue;
}
else if(s > 0){
buf[s] = '\0';
printf("[%s:%d]# %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//sendto類似於connect,這一步是在進行伺服器回顯
}
else{
printf("client exit!\n");
continue;
}
}
return 0;
}
client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
static Usage(const char* proc)
{
printf("Usage:%s[server_ip][server_prot]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3){
Usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);//create socket
if(sock < 0){
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
while(1)
{//先寫再讀
printf("client#");
fflush(stdout);
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0){
buf[s-1] = '\0';
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
ssize_t _s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);//實現伺服器回顯
if(_s > 0){
buf[_s] = '\0';
printf("[%s:%d]:%s\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
}
}
else if(s == 0){
printf("read ending\n");
exit(4);
}
else{
perror("read");
break;
}
}
close(sock);
return 0;
}
client1
client2
server