基於linux系統的CS模型實現

(一) CS模型

也就是TCP的客戶/服務器模型,我們這裏用一個簡單的回射客戶/服務器模型來進行模擬驗證。客戶發送數據,服務器接收到數據,並將數據原封不動的返回給客戶端。

CS模型

(二)用到的函數

1.socket()函數

1)函數原型:
int socket(int domain, int type, int protocol);
2)功能:
創建一個套接字用於通信。
3)參數:
1.domain:即協議域,又稱爲協議族(family):

這裏寫圖片描述

2.type:指定socket類型:常用的socket類型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(數據包套接字)。

這裏寫圖片描述

3.protocol:公共協議;
常用的公共協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議。

注意:上面的type和protocol不是可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol爲0時,會自動選擇type類型對應的默認協議。

4.返回值:成功返回非負,失敗返回-1。
5.errno
函數socket()並不總是執行成功,有可能會出現錯誤,錯誤的產生有多種原因,可以通過errno獲得:

這裏寫圖片描述

2.bind()函數

1)作用:將特定的ip地址,port端口號綁定到socket上;bind()函數把一個地址族中的特定地址賦給socket。如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。將socket與你本機上的一個端口相關聯(往往當你在設計服務器端程序時需要調用該函數。隨後你就可以在該端口監聽服務請求;而客戶端一般無須調用該函數)。

2)原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

3)參數:
1.sockfd:即socket描述字:
它是通過socket()函數創建了,唯一標識一個socket。

addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址,包含有關你的地址的信息:名稱、端口和IP 地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同。
如ipv4對應的是: 
struct sockaddr_in {
sa_family_t    sin_family; 
in_port_t      sin_port;   
struct in_addr sin_addr;   /* internet address */
//通常設定設定sin_addr爲INADDR_ANY(表示任意的意思)
};

/* Internet address. */
/*sin_addr結構體中只有一個唯一的字段s_addr,表示IP地址,該字段是一個整數,一般用函數inet_addr()把字符串形式的IP地址轉換成unsigned long型的整數值後再置給s_addr。*/
struct in_addr {
uint32_t       s_addr;    
};

可以用下面的賦值實現自動獲得本機IP地址和隨機獲取一個沒有被佔用的端口號: 
my_addr.sin_port = 0;  /* 系統隨機選擇一個未被使用端口號 */ 
my_addr.sin_addr.s_addr = INADDR_ANY;  /* 填入本IP地址 */ 

 注意:
 1.服務程序在爲其socket綁定IP地址時可以把htonl(INADDR_ANY)置給s_addr,這樣做的好處是不論哪個網段上的客戶程序都能與該服務程序通信;
 2.如果只給運行在多宿主機上的服務程序的socket綁定一個固定的IP地址,那麼就只有與該IP地址處於同一個網段上的客戶程序才能與該服務程序通信。   

ipv6對應的是: 
struct sockaddr_in6 { 
sa_family_t     sin6_family;   /* AF_INET6 */ 
in_port_t       sin6_port;     /* port number */ 
uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
struct in6_addr sin6_addr;     /* IPv6 address */ 
uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
unsigned char   s6_addr[16];   /* IPv6 address */ 
};

Unix域對應的是: 
#define UNIX_PATH_MAX    108
struct sockaddr_un { 
sa_family_t sun_family;               /* AF_UNIX */ 
char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

3)addrlen:對應的是地址的長度。

4)通常服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲什麼通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

5)返回值:
bind()函數在成功被調用時返回0,當bind()函數調用錯誤的時候,它也是返回–1 作爲錯誤發生的標誌。errn 的值爲錯誤代碼。小於1024 的所有端口都是保留下來作爲系統使用端口的,沒有root 權利無法使用。你可以使用1024 以上的任何端口,一直到65535。調用bind()的常見錯誤是EADDRINUSE,即指定的地址正在使用,主要是指定的端口號被使用了,IP地址可以被多個進程使用,但端口在同一時刻只能被一個進程使用。

6)bind範例:
下面是一個bind函數調用的例子:
struct sockaddr_in saddr;
memset((void *)&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = htonl(INADDR_ANY); 
//saddr.sin_addr.s_addr = inet_addr("192.168.22.5"); 綁定固定IP
bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));

3.網絡字節序 與 主機字節序

(1)主機字節序:主機字節序就是我們平常說的大端和小端模式:不同的CPU有不同的字節序類型。這些字節序是指整數在內存中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:


a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。如32位的十六進制數0x12345678存儲在4個字節的內存中爲:78存儲在第一個字節,56存儲在第二個字節,34爲第三個字節,12爲第四個字節。


b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。如32位的十六進制數0x12345678存儲在4個字節的內存中爲:12存儲在第一個字節,34存儲在第二個字節,56爲第三個字節,78爲第四個字節。

注意:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序一樣使用的是Big-Endian。由於這個問題曾引發過血案!公司項目代碼中由於存在這個問題,導致了很多莫名其妙的問題,所以請謹記對主機字節序不要做任何假定,務必將其轉化爲網絡字節序再賦給socket。

htonl():
1)原型:

2)背景:計算機數據存儲有兩種字節優先順序:高位字節優先和低位字節優先。Internet上數據以高位字節優先順序在網絡上傳輸,所以對於在內部是以低位字節優先方式存儲數據的機器,在Internet上傳輸數據時就需要進行轉換。

幾個字節順序轉換函數: 
htons()--"Host to Network Short" ; htonl()--"Host to Network Long" 
ntohs()--"Network to Host Short" ; ntohl()--"Network to Host Long" 

4.listen()函數

1)原型:

2)作用:調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。

3)參數:
sockfd 是一個套接字描述符,爲要監聽的socket描述字。
backlog相應socket可以排隊的最大連接個數。backlog 具體一些是什麼意思呢?每一個連入請求都要進入一個連入請求隊列,等待listen 的程序調用accept()(accept()函數下面有介紹)函數來接受這個連接。當系統還沒有調用accept()函數的時候,如果有很多連接,那麼本地能夠等待的最大數目就是backlog 的數值。

4)返回值:
listen()如果返回 –1 ,那麼說明在listen()的執行過程中發生了錯誤。

5)注意:
socket()函數創建的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的連接請求。

5.connect()函數

1)原型:
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);

2)參數:
 sockfd :套接字文件描述符,由socket()函數返回的,此中爲客戶端的sockfd。
 serv_addr 是一個存儲遠程計算機的IP 地址和端口信息的結構,一般爲服務器的ip與port的結構。
 addrlen 應該是sizeof(struct sockaddr)。

3)作用:
客戶端通過調用connect函數來建立與TCP服務器的連接。

6.accept()函數

過程1:TCP服務器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之後就想TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。之後就可以開始網絡I/O操作了,即類同於普通文件的讀寫I/O操作。


過程2:
1.有人從很遠很遠的地方嘗試調用connect()來連接你的機器上的某個端口(當然是你已經在listen()的)。
2.他的連接將被listen 加入等待隊列等待accept()函數的調用(加入等待隊列的最多數目由調用listen()函數的第二個參數backlog 來決定)。
3.你調用accept()函數,告訴他你準備連接。accept函數將回返回一個新的套接字描述符,這個描述符就代表了這個連接!


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

2)參數:
sockfd :爲服務器的socket描述字;
addr:存儲着遠程連接過來的計算機的信息(比如遠程計算機的IP 地址和端口)。用於返回客戶端的協議地址。
addrlen:sizeof(struct sockaddr_in)

3)返回值:
如果accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。

4)注意:
1.accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字.
2.accept函數返回的是已連接的socket描述字.
3.一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命週期內一直存在。

服務器端代碼&&客戶端代碼

服務器端代碼

#include<unistd.h> 
#include<sys/socket.h> 
#include<netinet/in.h> 
#include<arpa/inet.h>
#include<stdio.h>

#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
    int main()
    {
    int listenfd;
    /*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
        ERR_EXIT("socket");

    //IPV4地址結構
    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");*/
    /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);

    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        ERR_EXIT("bind");


    if(listen(listenfd,SOMAXCONN)<0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
    int conn;//已連接套接字
    if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
        ERR_EXIT("accept");
    char recvbuf[1024];
    while(1){
        memset(recvbuf,0,sizeof(recvbuf));//初始化recvbuf
        int ret=read(conn,recvbuf,sizeof(recvbuf));//函數從打開的文件,設備中讀取數據,返回讀取的字節數。
        fputs(recvbuf,stdout);
        write(conn,recvbuf,ret);//buf中數據被複制到了TCP發送緩衝區
    }
    close(conn);
    close(listenfd);
    return 0;

}

客戶端代碼
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>

    #include<stdio.h>
    #include<stdlib.h>
    #include<errno.h>
    #include<string.h>
    #define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
    int main()
    {
    int sock;
    /*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
    if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
        ERR_EXIT("socket");

    //IPV4地址結構
    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");
    /*inet_aton("127.0.0.1",&servaddr.sin_addr);*/


    if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        ERR_EXIT("connect");

    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);
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
    close(sock);
    return 0;

}

“`

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章