1. 程式人生 > >Socket程式設計(二)回射客戶/伺服器的實現

Socket程式設計(二)回射客戶/伺服器的實現

TCP客戶/伺服器模型,即C/S模型

可以將這個模型類比為打電話。

TCP伺服器通過socket()建立一個套接字,相當於安裝一臺話機;然後為安裝好的話機繫結bind()一個電話號碼;讓話機處於監聽的狀態listen();等待客戶端的連線accept(),也就是等待對方電話撥打過來,如果一直沒有電話撥打過來,那就一直阻塞,知道客戶端連線到達;

對於客戶端來說,它也需要安裝一部話機socket();接下來呼叫connect()函式,撥打電話號碼;撥通之後就建立了連線,一旦連線建立之後雙方就開始進行通訊;

客戶端通過write()函式發起請求;伺服器通過read()函式接收請求,接收到請求後對請求進行處理;然後伺服器通過write()函式將處理的結果向客戶端進行應答;客戶端通過read()函式讀取伺服器發來的應答;這是一個迴圈,如果客戶端還有請求可繼續傳送,伺服器會根據客戶端傳送的請求進行應答;

在通訊過程中,任何一端都可以終止通訊,通過呼叫close()函式來終止通訊;比如客戶端發起一個close,這是一個檔案描述符EOF的通知,伺服器讀到read()通知就會呼叫close()將自己的套接字關閉掉。

這就是一個基本的C/S模型

回射客戶/伺服器模型

客戶端從標準輸入(stdin)選擇一行資料,通過網路傳送(write)給伺服器一端,伺服器讀取(read)資料,不做任何處理,將資料原封不動的傳送(write)給客戶端,客戶端讀取資料(read)並標準輸出(stdout)。

這是一個基本的回射客戶/伺服器模型,下面是實現這個模型所用到的函式:

socket函式

包含標頭檔案:<sys/socket.h>

功能:建立一個套接字用於通訊

函式原型:int socket(int domain,int type,int protocol);

相關引數:domain指定通訊協議族;type指定socket型別,流式套接字SOCK_STREAM、資料包套接字SOCK_DGRAM、原始套接字SOCK_RAW;protocol指定協議型別。

返回值:成功返回非負整數,它與檔案描述符類似,我們把它稱為套介面描述字,簡稱套接字;失敗返回-1 .

bind函式

包含標頭檔案:<sys/socket.h>

功能:繫結一個本地地址到套接字

函式原型:int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);  //將IPv4的地址強制轉化為通用地址長度

相關引數:sockfd是socket函式返回的套接字;addr是要繫結的地址;addrlen是地:址長度。

返回值:成功返回0,失敗返回-1;

listen函式

包含標頭檔案:<sys/socket.h>

功能:將套接字用於監聽進入的連線,將套接字從close狀態轉換成監聽(listen)狀態

函式原型:int  listen(int sockfd,int backlog);

相關引數:sockfd是socket函式返回的套接字;backlog規定核心為此套接字排隊的最大連線個數。

返回值:成功返回0,失敗返回-1.

一旦呼叫listen函式後,這個套接字就變成了被動套接字,否則預設是主動套接字。主動套接字是用來發起連線的(通過呼叫connect函式),被動套接字是用來接收連線的(通過呼叫accept函式)。

一般來說,listen函式應該在呼叫socket和bind函式之後,呼叫函式accept之前呼叫。

對於給定的監聽套介面,核心要維護兩個佇列:已由客戶端發出併到達伺服器,伺服器正在等待完成響應的TCP三次握手過程;已完成連線的佇列。

連線需要通過三次握手,三次握手時會發送一些SYN分節過來,這時需要兩個佇列來維護,一是未完成連線佇列,一旦三次握手成功之後,就會將這個分節移動到已完成連線佇列裡邊。listen函式的第二個引數backlog代表未完成連線佇列與已完成連線佇列之和。伺服器呼叫accept函式之後就會從已完成連線佇列取走一個SYN分節,以便更多的客戶端可以發起連線。所以backlog規定了能夠併發連線過來的套介面數目,它的值等於未完成連線數目和已完成連線數目。

accept函式

包含標頭檔案:<sys/socket.h>

功能:從已完成佇列返回第一個連線,如果已完成連線佇列為空,則阻塞。

函式原型:int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);

相關引數:sockfd是伺服器套接字;addr將返回對等方的套接字地址;addrlen返回對等方的套接字地址長度。

返回值:成功返回非負整數,表示建立了一個新的套接字;失敗返回-1。

connect函式

包含標頭檔案:<sys/socket.h>

功能:建立一個連線至addr所指定的套接字

函式原型:int connect(int sockfd,const struct sockaddr*addr,socklen_t addrlen);

相關引數:sockfd是未連線的套接字;addr是要連線的套接字地址;addrlen是第二個引數addr的長度。

返回值:成功返回0,失敗返回-1.

以下是回射客戶/伺服器程式碼

伺服器端echo-server.c

#include <sys/types.h>         
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m)

int main(void)
{
    //create a socket
    int sockfd;
    if((sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))<0)  //前兩個引數已經決定是TCP協議
                                                            //第三個引數可以直接寫0,如下
    /*if((sockfd=socket(AF_INET,SOCK_SCREAM,0))<0);  //SOCK_SCREAM TCP 0 */
          ERR_EXIT("socket error!");
    
    //initialize address
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(5188); //將主機位元組序轉化為網路位元組序,s代表兩個位元組。
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //0.0.0.0 all of the host address
                                                //這裡的htonl可以省略掉,因為全0,轉化也為0
    /*servaddr.sin_addr.s.addr=inet_addr("127.0.0.1"); //將點分式十進位制地址轉化為32位地址*/
    /*inet_aton("127.0.0.1",&servaddr.sin_addr); //將點分式十進位制地址轉化位網路地址*/
    
    //bind將套接字與本地地址進行繫結
    if(bind(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
          ERR_EXIT("bind error!"); 
    
    //listen將套接字用於監聽進入的連線
    if(listen(sockfd,SOMAXCONN)<0) //SOMAXCONN這個巨集代表隊列最大值
          ERR_EXIT("listen error!"); 

    //accept從已完成連線佇列的隊頭得到一個連線
    struct sockaddr_in peeraddr;  //define peer address
    socklen_t peerlen=sizeof(peeraddr); //對方的地址長度
    int conn;  //define a new socket,the accept will return it.
    if((conn=accept(sockfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
          ERR_EXIT("accept error!");

    //通訊
    char recvbuf[1024];
    while(1)
    { 
        memset(recvbuf,0,sizeof(recvbuf));
        int ret=read(conn,recvbuf,sizeof(recvbuf));//讀取新的套接字,這個套接字由accept返回
        fputs(recvbuf,stdout);
        write(conn,recvbuf,ret);
    }

    close(conn);
    close(sockfd);

    return 0;

}

客戶端echo-client.c

#include <sys/types.h>         
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m)

int main(void)
{
    //create a socket
    int sock;
    if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
    /*if((sock=socket(AF_INET,SOCK_SCREAM,0))<0);  //SOCK_SCREAM TCP 0 */
          ERR_EXIT("socket error!");
    
    //initialize address
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(5188);
    servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//連線伺服器端的地址
    
    //不需要繫結、監聽,直接連線即可
    
    //connect發起連線
    if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
          ERR_EXIT("connect error!");

    //通訊
    char sendbuf[1024]={0};
    char recvbuf[1024]={0};
    while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)  //從標準輸入接收一行資料
    {
        write(sock,sendbuf,strlen(sendbuf));
        read(sock,recvbuf,sizeof(recvbuf));
        fputs(recvbuf,stdout);  //回射資料

        //clear the buf
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
     
    close(sock);
    return 0;

}