網絡編程——CS模型(總結)

什麼是socket

將底層複雜的協議體系,執行流程,進行了封裝,封裝完的結果,就是一個SOCKET,也就是說,SOCKET是咱們調用協議進行通訊的操做接口數組

數據類型:SOCKET 轉定義:unsigned int服務器

在系統裏每個socket對應着==惟一的一個整數==,好比23,對應着socket的協議等信息,在通訊中,就使用這些整數進行通訊,系統會自動去找這些整數所對應的協議

應用

每一個客戶端有一個socket,服務器有一個socket,通訊時就是經過socket,來表示和誰傳遞信息網絡


建立socket

/* 函數原型 */
SOCKET socket(
  int af,       /*地址的類型*/
  int type,     /*套接字類型*/
  int protocol  /*協議類型*/
);
參數1:地址類型
地址類型 形式
==AF_INET== 192.168.1.103(IPV4,4字節,32位地址)
AF_INET6 2001:0:3238:DFE1:63::FEFB(IPV6,16字節,128位地址)
AF_BTH 6B:2D:BC:A9:8C:12(藍牙)
AF_IRDA 紅外
通訊地址不止只有IP地址
參數2:套接字類型
類型 用處
==SOCK_STREAM== 提供帶有OOB數據傳輸機制的順序,可靠,雙向,基於鏈接的字節流。 使用傳輸控制協議(TCP)做爲Internet地址系列(AF_INET或AF_INET6)
SOCK_DGRAM 支持數據報的套接字類型,它是固定(一般很小)最大長度的無鏈接,不可靠的緩衝區。使用用戶數據報協議(UDP)做爲Internet地址系列(AF_INET或AF_INET6)
SOCK_RAW 提供容許應用程序操做下一個上層協議頭的原始套接字。 要操做IPv4標頭,必須在套接字上設置IP_HDRINCL套接字選項。 要操做IPv6標頭,必須在套接字上設置IPV6_HDRINCL套接字選項
SOCK_RDW 提供可靠的消息數據報。 這種類型的一個示例是Windows中的實用通用多播(PGM)多播協議實現,一般稱爲可靠多播節目
SOCK_SEQPACKET 提供基於數據報的僞流數據包
參數3:協議類型
協議類型 用處
IPPROTO_TCP 傳輸控制協議(TCP)
IPPROTO_UDP 用戶數據報協議(UDP)
IPPROTO_ICMP Internet控制消息協議(ICMP)
IPPROTO_IGMP Internet組管理協議(IGMP)
IPPROTO_RM 用於可靠多播的PGM協議

==填寫0==表明系統自動幫咱們選擇協議類型socket

== 參數一、二、3是要相互配套使用的==,不能隨便填,使用不一樣的協議就要添加不一樣的參數
返回值
  • 成功返回可用的socket變量
  • 失敗返回INVALID_SOCKET,可使用WSAGetlasterror()返回錯誤碼
if (INVALID_SOCKET == socketServer)
{
    int a = WSAGetLastError( );
    WSACleanup();
    return 0;
}

建立socket代碼

SOCKET socketListen = socket(AF_INET,SOCK_STREAM,0);
if(INVALID_SOCKET == socketListen)
{
    int a = WSAGetLastError( );
    WSACleanup();
    return 0;
}

bind()函數

做用:給socket綁定端口號與地址tcp

  • 地址:IP地址
  • 端口號函數

    • 同一個軟件可能佔用多個端口號(不一樣功能)
    • 每一種通訊的端口號是惟一的
int bind
(
  SOCKET s,    /*服務器建立的socket*/
  const sockaddr *addr,  /*綁定的端口和具體地址*/
  int namelen  /*sizeof(sockaddr)*/
);
參數1:==被綁定socket變量==
參數2:綁定端口號和地址

定義一個==SOCKADDR_IN==數據類型,是一個結構體:測試

typedef struct sockaddr_in {

#if(_WIN32_WINNT < 0x0600)
    short   sin_family; /* 地址類型 */
#else //(_WIN32_WINNT < 0x0600)
    ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)

    USHORT sin_port;   /* 端口號 */
    IN_ADDR sin_addr;  /* IP地址 */
    CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
其中IN_ADDR sin_addr; 又是一個結構體
typedef struct in_addr {
        union {
                struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { USHORT s_w1,s_w2; } S_un_w;
                ULONG S_addr;
        } S_un;
#define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2    // host on imp
#define s_net   S_un.S_un_b.s_b1    // network
#define s_imp   S_un.S_un_w.s_w2    // imp
#define s_impno S_un.S_un_b.s_b4    // imp #
#define s_lh    S_un.S_un_b.s_b3    // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
127.0.0.1 本地迴環地址,用於本地網絡測試,數據不出計算機

端口號:0~65535spa

  • 0~1013:系統保留佔用端口號調試

    • 21端口分配給FTP(文件傳輸協議)服務
    • 25端口分配給SMTP(簡單郵件傳輸協議)服務
    • 80端口分配給HTTP服務
  • 1024~5000:不少系統把這個取餘分配給客戶端
  • ==5000~65535==:咱們選用的最佳範圍
  • 49151~65535:系統動態隨機端口
查看端口號使用狀況
  • 打開運行cmd輸入netstat -ano ->查看被使用的全部端口
  • netstat -ano|findstr 「端口號」 ->檢查端口號是否被使用了
si.sin_port = ==htons(12345)==;
si.sin_addr.S_un.S_addr = ==inet_addr("127.0.0.1")==;
si.sin_family = ==AF_INET==;
參數3:參數2類型大小

sizeof(sockadd)code

返回值
  • 成功返回0
  • 失敗返回SCOKET_ERROR
if (SOCKET_ERROR == bind(socketListen,(struct sockaddr*)&sockAddress,sizeof(sockAddress)))
{
    printf("bind fail!");
    //int nError = ::WSAGetLastError();
    //關閉庫
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

綁定端口與地址代碼

struct SOCKADDR_IN si;
si.sin_family = AF_INET;   /* 地址協議 */
si.sin_port = htons(12345);  /* 端口號 */
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  /*IP地址,點分十進制*/

if (SOCKET_ERROR == bind(socketListen,(struct sockaddr*)&sockAddress,sizeof(sockAddress)))
{
    printf("bind fail!");
    //int nError = ::WSAGetLastError();
    //關閉庫
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

listen()函數

做用:將socket置於偵聽傳入鏈接的狀態(服務器能夠接受客戶端連接了)

int WSAAPI listen
(
  SOCKET s,  /*服務器端的socket*/
  int backlog  /*掛起鏈接隊列的最大長度*/
);
參數1:==接受連接的socket==
參數2:掛起鏈接隊列的最大長度
好比有100個用戶連接請求,可是系統一次只能處理20個,那麼剩下的80個不能不理人家,因此係統就建立個隊列記錄這些暫時不能處理,過一下子處理的連接請求,依前後順序處理,那這個隊列到底多大?就是這個參數設置,好比2,那麼就容許兩個新連接排隊的。這個確定不能無限大,那內存不夠了。
  • 填寫==SOMAXCONN==,讓系統自動選擇最合適的個數
返回值
  • 成功返回0
  • 失敗返回SOCKET_ERROR
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
    int a = WSAGetLastError();
    WSACleanup();
    return 0;
 }

開啓監聽代碼

if (SOCKET_ERROR == listen(socketListen,2))
{
    printf("listen fail!");
    //關閉庫
    closesocket(socketListen);
    WSACleanup();
    return -1;
}

accept()

在服務器端上建立一個新的socket,將客戶端的信息和新的socket綁定在一個,一次只能建立一個

SOCKET WSAAPI accept
(
  SOCKET s,   /*服務器的socket*/
  sockaddr *addr,  /*返回客戶端地址端口信息結構體*/
  int *addrlen   /*返回參數2的類型大小*/
);
參數1:==服務器的socket==
  • 爲何是服務器的socket,不是客戶端的socket?

理解:經過服務器端的socket,讀取客戶端的信息

參數2:客戶端端口地址信息結構體
  • bind()的第二個參數同樣(地址類型、端口號、IP地址)
  • 系統會幫咱們自動填寫,因此咱們能夠設置爲==NULL==
/*直接經過函數獲得客戶端的端口號、IP地址*/
getpeername(newSocket,(struct sockaddr *)&sockClient,&nLen);
/*獲得本地服務器信息*/
getsockname(sSocket,(struct sockaddr *)&addr,&nLen);
參數3:返回參數2的數據類型大小

若是參數二、3都填==NULL==,那麼就是不直接獲得客戶端的地址和端口號

返回值
  • 成功返回創建好的客戶端socket
  • 失敗返回INVALID_SOCKET
if (INVALID_SOCKET == socketClient)
{
    int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();
}
accept() 返回一個新的套接字來和客戶端通訊,addr保存了客戶端的IP地址和端口號,而s是服務器端的套接字,注意區分。接下來和客戶端通訊時,要使用這個新生成的套接字,而不是原來服務器端的套接字。
SOCKET newSocket;
newSocket = accept(socketListen, NULL, NULL);
if (INVALID_SOCKET == newSocket)
{
    printf("listen fail!" );
    //關閉庫
    closesocket(socketServer);
    WSACleanup();
}

accept調試

  • ==阻塞==、同步

沒有客戶端連接就一直卡着

  • ==一次只能連接一個==

recv()

  • 做用:獲得指定客戶端發來的消息
  • 原理:複製
數據的接收都是協議自己在作,也就是socket底層在操做,系統有一段緩衝區,儲存着接收到的數據。

recv的做用,就是經過socket找到了這個緩衝區,把數據複製放到本身的數組中

int recv
(
  SOCKET s,   /*客戶端的socket,每一個客戶端對應惟一的socket*/
  char *buf,  /*數據緩衝區*/
  int len,    /*數據長度*/
  int flags   /*讀取方式*/
);
參數1:==接收端的socket==
參數2:客戶端消息的存儲空間,==字符數組==,通常不大於1500字節
  • 爲何不大於1500字節?

解釋:由於網絡傳輸的最大單元是1500字節,這是協議規定的

參數3:存儲空間的大小(字節)

通常是==參數2的字節-1==,把「0」字符串結尾保留下來

參數4:數據讀取方式
讀取方式 做用
==0== 從系統緩衝區讀到buf緩衝區,將系統緩衝區的數據刪掉,讀出來就刪
MSG_PEEK 數據複製到buf緩衝區,可是數據不從系統緩衝區刪除
MSG_OOB 傳輸一段數據,再外帶加一個額外的特殊數據
MSG_WAITTALL 直到系統緩衝區字節數知足參數3的數目,纔開始讀取
返回值
  • 成功返回==讀出來的字節大小==
  • 客戶端下線,返回==0==
  • 失敗返回==SOCKET_ERROR==

接受數據代碼

char szRecvBuffer[1500] = {0};  /* 字符數組 */
int nReturnValue = recv(newSocket, szRecvBuffer, sizeof(szRecvBuffer)-1, 0);
if (0 == nReturnValue)
{
    //客戶端正常關閉   服務端釋放Socket
    continue ;
}
else if (SOCKET_ERROR == nReturnValue)
{
    //網絡中斷  
    printf("客戶端中斷鏈接");
    continue;    
}
else
{
    //接收到客戶端消息 
    printf("Client Data : %s \n",szRecvBuffer);
}

send()

  • 做用:向客戶端發送數據
  • 原理:將咱們的數據粘貼進系統系統緩衝區,交給系統乘機發送出去
int WSAAPI send
(
  SOCKET s,  /*客戶端socket*/
  const char *buf,  /*發送的字符數組*/
  int len,  /*發送長度*/
  int flags  /*發送方式*/
);
參數1:==發送端的socket==
參數2:給客戶端發送的==字符數組==,不大於1500字節
  • 不要超過1500字節

發送的時候,協議要進行包裝,加上協議信息(包頭),鏈路層14字節,ip頭20字節,tcp頭20字節,數據結尾還要有狀態確認,加起來也幾十個字節,因此不能寫1500個字節,最多1400字節(或者1024)。

  • 若是超過1500字節

系統會分片處理,分兩個包,假設2000字節的包,1400+包頭=1500,600+包頭=700。那麼系統就要分包->打包->發送,客戶端接收到要拆包->組合數據。

參數3:==發送字節數==
參數4:發送方式
發送方式 做用
==0== 默認
MSG_OOB 傳輸一段數據,再外帶加一個額外的特殊數據
MSG_DONTROUTE 數據不該受路由限制
返回值
  • 成功返回發送的字節數
  • 失敗返回SOCKET_ERROR(客戶端正常下線也是返回這個)
if (SOCKET_ERROR == send(socketClient, "abcd", sizeof("abcd"), 0))
{
    //出錯了
    int a = WSAGetLastError();
    //根據實際狀況處理
}

發送數據代碼

char szSendBuffer[1024]; /*發送字符數組*/
send(newSocket, "repeat over", strlen(szSendBuffer)+1, 0);

connect()

做用:客戶端連接服務器端,將本機的一個指定的socket鏈接到一個指定地址的服務器socket上去

理解:connect將在本機和指定服務器間創建一個鏈接。但實際上,connect操做並不引起網絡設備傳送任何的數據到對端。它所 作的操做只是經過路由規則和路由表等一些信息,在struct socket結構中填入一些有關對端服務器的信息。這樣,之後向對端發送數據報時,就不須要每次進行路由查詢等操做以肯定對端地址信息和本地發送接口,應用程序也就不須要每次傳入對端地址信息
int WSAAPI connect
(
  SOCKET s,  /*客戶端建立的連接服務器的socket*/
  const sockaddr *name, /*服務器IP地址端口號結構體*/
  int namelen  /*sizeof(sockaddr)*/
);
參數1:客戶端建立的用來連接服務器的socket
參數2:服務器的IP地址和端口號
參數3:參數2的結構體大小
返回值
  • 成功返回發送的字節數
  • 失敗返回SOCKET_ERROR
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
{
    int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();
}

客戶端連接服務器代碼

struct sockaddr_in serverMsg;
serverMsg.sin_family = AF_INET;  /*地址類型*/
serverMsg.sin_port = htons(12345); /*服務器端口*/
serverMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); /*服務器IP*/
connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg));
if (SOCKET_ERROR == connect(socketServer, (struct sockaddr *)&serverMsg, sizeof(serverMsg)))
{
    int a = WSAGetLastError();
    closesocket(socketServer);
    WSACleanup();
}

圖片描述


CS模型存在的問題

accept()、recv()阻塞問題

  • 因爲accept()recv()是阻塞的,作其中一件事,另一件事就作不了。

    • 若是咱們在等着收消息recv,來了一個連接請求,那就沒法處理。
    • 若是等的socket沒有發送請求,也是一直等。

解決辦法

  • 咱們能夠主動和系統要有請求的socket

    • 獲得連接請求,處理accept函數
    • 獲得發來的消息,處理recv函數

解決問題

  • 爲何服務器socket有響應的時候就是accept?

答:由於服務器接收、發送數據都是經過綁定客戶端信息的socket進行的,不是經過服務器socket,服務器socket只是接受客戶端請求的連接,而且把客戶端的信息綁定到一個新的socket上,之後的通訊都是經過這個socket,因此服務器有響應就是有新的請求連接

  • 爲何客戶端socket從頭至尾都是用的同一個socket?

答:客戶端所建立的socket只是本機和指定服務器間創建一個鏈接,socket結構中填入一些有關對端服務器的信息。這樣,之後向對端發送數據報時,就不須要每次進行路由查詢等操做以肯定對端地址信息和本地發送接口,能夠理解爲客戶端的所建立的socket其實就是和服務器數據交換的socket,與服務器端最開始建立的socket不一樣。

相關文章
相關標籤/搜索