1. 程式人生 > >【網路】實現簡單的TCP、UDP伺服器、TCP多程序/多執行緒伺服器

【網路】實現簡單的TCP、UDP伺服器、TCP多程序/多執行緒伺服器

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
這裡寫圖片描述