1. 程式人生 > 實用技巧 >伺服器、客戶端

伺服器、客戶端

編寫一個伺服器socket程式步驟:

初始化DLL->建立套接字->繫結套接字->進入監聽狀態->等待客戶端連線->接受客戶端請求->向客戶端傳送資料->關閉套接字->終止DLL使用

標頭檔案

//注意將WinSock2放在之前 否則會報錯
//或者使用巨集定義
#include<WinSock2.h>
#include<Windows.h>
//明確指定需要一個動態庫
//在工程中聯結器中加入lib動態庫
#pragma comment(lib,"ws2_32.lib")

初始化DLL

   //呼叫windows庫檔案
    
//啟動Windows socket 2.x環境 WORD ver = MAKEWORD(2, 2); WSADATA dat; WSAStartup(ver, &dat);

建立套接字

建立套接字需要使用到socket()函式。

Linux:intsocket(intaf,inttype,intprotocol);

Windows:SOCKETsocket(int af,int type,int protocol);

  //1、建立一個socket 套接字
    SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

其中的區別是返回型別不同,Linux下返回的是int型的檔案描述符,而Windows下返回的是SOCKET型別的套接字。

Linux中一切都是檔案,每個檔案都有一個整數型別的檔案描述符。socket也是一個檔案,使用socket建立套接字後就返回一個int型別的檔案描述符。

Windows下回區分普通檔案和socket檔案,在使用socket建立套接字後返回值型別為SOCKET型別用於表示一個套接字。

intaf:為地址族(AddressFamily),就是IP的型別型(AF_INET,AF_INET6)分別代表IPv4和IPv6地址。

例:127.0.0.1就是一個IPv4地址,通常用來表示本機地址。

inttype:表示資料常熟方式/套接字型別。

SOCK_STREAM(流資料格式/面向連線的套接字):一種可靠地、雙向的資料通訊資料流,資料有誤,可以重新發送。一般使用TCP協議。

流格式套接字的內部有一個緩衝區(字元陣列),通過socket傳輸的資料將儲存到這個緩衝區。接收端可以選擇性的讀取緩衝區的內容。

瀏覽器所使用的http協議就是基於面向連線的套接字。

SOCK_DDGRAM(資料報套接字/無連線的套接字):面向無連線的,傳輸資料中不做資料校驗,如果資料出錯,無法重傳。也正因如此在傳輸效率上要比流資料格式套接字。一般使用UDP協議。

QQ視訊聊天和微信語音聊天使用到SOCK_DDGRAM傳輸資料。

intprotocol:表示傳輸協議,IPPROTO_TCPIPPTOTO_UDP協議分別表示TCP傳輸協議和UDP傳輸協議。

繫結套接字

繫結套接字需要使用bind()函式。

Linux:intbind(int sock,struct sockaddr* addr, socklen_t addrlen);

Windows:int bind(SOCKET sock, const struct sockaddr* addr, int addrlen);

  //2、bind 繫結用於接受客戶端連線的 網路埠
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);//繫結一個埠號
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");    //繫結到那個ip地址
    bind(_sock, (sockaddr*)&_sin, sizeof(_sin))  //將套接字和IP、埠繫結

SOCKET sock:表示前面使用socket()函式建立的套接字物件。

const struct sockaddr* addr:一個結構用來表示IP、埠號等

建立sockaddr_in結構體,通過該結構體設定IP地址127.0.0.1、埠2345。

sockaddr_in結構體如下所示:

  typedef struct sockaddr_in {
  #if(_WIN32_WINNT < 0x0600)
      short   sin_family;   //地址族(Address Family),也就是地址型別
  #else //(_WIN32_WINNT < 0x0600)
      ADDRESS_FAMILY sin_family;
  #endif //(_WIN32_WINNT < 0x0600)
      USHORT sin_port;     //16位的埠號
      IN_ADDR sin_addr;   //32位IP地址
      CHAR sin_zero[8];    //不使用,一般用0填充
  } SOCKADDR_IN, *PSOCKADDR_IN;

其中巨集定義表示:當Windows的版本小於0x0600使用地址族型別為short型,否則使ADDRESS_FAMILY型別。ADDRESS_FAMILY就是unsignedshort型別。typedef unsigned short USHORT;

sin_family:和socket()的第一個引數的含義相同,取值也必須保持一致。

sin_port:埠號,兩個位元組16位表示,理論上埠號的取值範圍為0~65536,0~1023的埠號一般有系統分配給特定的服務程式,如:Web:80、FTP:21。因此最好使用1024~65536之間的埠號。

sin_addr:是一個結構體如下所示。只需要知道使用來繫結IP就行。暫時還搞不太清???

  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;

int addrlen:為addr變數的大小,可以使用sizeof計算出來。

問題:為什麼使用sockaddr_in而不使用bind()函式要求的sockaddr?

參考:http://c.biancheng.net/view/2344.html

sockaddr 和 sockaddr_in 的長度相同,都是16位元組,只是將IP地址和埠號合併到一起,用一個成員 sa_data 表示。要想給 sa_data 賦值,必須同時指明IP地址和埠號,例如”127.0.0.1:80“,遺憾的是,沒有相關函式將這個字串轉換成需要的形式,也就很難給 sockaddr 型別的變數賦值,所以使用 sockaddr_in 來代替。這兩個結構體的長度相同,強制轉換型別時不會丟失位元組,也沒有多餘的位元組。
可以認為,sockaddr 是一種通用的結構體,可以用來儲存多種型別的IP地址和埠號,而 sockaddr_in 是專門用來儲存 IPv4 地址的結構體。另外還有 sockaddr_in6,用來儲存 IPv6 地址

進入監聽狀態

被動進入監聽狀態需要使用liste()函式。

Linux:int listen(int sock, int backlog);

Windows:int listen(SOCKET, int backlog);

SOCKETsock:表示前面使用socket()函式建立的套接字物件。

int backlog:表示請求佇列的最大長度。

  //3、listen 監聽網路介面
  SOCKET_ERROR == listen(_sock, 5)

被動監聽:只有當客戶端請求時,套接字才進入“喚醒”狀態來響應請求。否則處於“睡眠”狀態

請求佇列:

套接字處理客戶端請求時,如果有新的請求,套接字將請求放入緩衝區中,處理完當前請求後,再從緩衝區中取出請求。如果不斷有新的請求進來,就按先後順序在緩衝區中排隊,直到緩衝區滿為止。這個緩衝區,可以稱為請求佇列。

listen()中第二個引數設定緩衝區的長度,設定為多少根據情況而定。設定為SOMAXCONN則由系統來解決。

當請求佇列滿了的時候,有新的請求就會發出請求錯誤。

等待客戶端連線

使用accept()函式等待客戶端請求。

Linux:int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);

Windows:SOCKET accept(SOCKET sock, struct sockaddr* addr, int* addrlen);

    //4、accept 等待接受客戶端連線
    sockaddr_in clientAddr = {};
    int nAddrLen = sizeof(sockaddr_in);
    SOCKET _cSock = INVALID_SOCKET;
    _cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);

struct sockaddr* addr:儲存的客戶端的IP和埠號,注意!!!

int* addrlen:表示addr的長度,使用sizeof()計算。

使用accept()函式會返回一個新的套接字和客戶端進行通訊,之後和客戶端進行通訊的時候使用這個新的套接字而不是之前的套接字。

accept()會阻塞程式的執行,直到有新的請求到來。

接受客戶端請求

接受客戶端請求使用read() / recv()函式。

Linux:ssize_t read(int fd,void*buf, size_t nbytes);

Windows:int recv(SOCKET sock,char* buf, int len, int flags);

  DataHeader header = {};
   //5、接受客戶端的請求資料
  int nLen = recv(_cSock, (char*)&header, sizeof(DataHeader), 0);

Linux來說:

int fd:要讀取的檔案描述符。

void* buf:接受資料的緩衝區地址。

size_t nbytes:要讀取的資料的位元組數。

read()函式會從fd檔案中讀取nbytes個位元組並儲存到緩衝區buf中,成功返回讀取到的位元組數,失敗返回-1.

Windows來說:

SOCKET sock:要接受的資料的套接字。

char* buf:要接受資料的緩衝區地址。

int len:接受的資料位元組數。

int flags:可選項,可以為0或NULL。先不用深究。

向客戶端傳送資料

客戶端傳送資料使用write() / send()函式。

Linux:ssize_t write(int fd, cons void* buf, size_t nbytes);

Windows:int send(SOCKET sock, const char* buf, int len, int flags);

  struct LoginResult
  {
      int result;
  };

  LoginResult ret = { 1 };
  //6、向客戶端傳送資料
  send(_cSock, (char*)&ret, sizeof(LoginResult), 0);

關閉套接字

關閉套接字使用closesocke()函式

  //7、closesocket關閉套接字
    closesocket(_sock);

終止DLL使用

    WSACleanup();

編寫一個客戶端socket程式步驟:

初始化DLL->建立套接字->連線伺服器->向伺服器傳送請求->接受伺服器返回資料->關閉套接字->終止DLL使用

建立套接字

與伺服器相同,請看伺服器處。

連線伺服器

連線伺服器使用connect()函式。

Linux:int connect(int sock, struct sockaddr* serv_addr, socklen_t addrlen);

Windows:int connect(SOCKET sock, const struct sockaddr* serv_addr, int addrlen);

    //2、連線伺服器
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));

SOCKET sock:客戶端使用socket()函式建立的socket。

const struct sockaddr* serv_addr:與客戶端bind()一樣,請檢視伺服器對應內容。

int addrlen:為serv_addr變數的大小,可以使用sizeof計算出來。

向伺服器傳送請求

與服務區傳送資料相同。write() / send()

接受伺服器返回資料

與伺服器接受請求相同。read() / recv()

關閉套接字

關閉套接字使用closesocke()函式。

完成程式碼:

伺服器:

#define WIN32_LEAN_AND_MEAN
#include<iostream>

//注意將WinSock2放在之前 否則會報錯
//或者使用巨集定義
#include<WinSock2.h>
#include<Windows.h>
//明確指定需要一個動態庫
//在工程中聯結器中加入lib動態庫
#pragma comment(lib,"ws2_32.lib")

enum CMD
{
    CMD_LOGIN,
    CMD_LOGOUT,
    CMD_ERROR
};
struct DataHeader
{
    short dataLength;
    short cmd;
};
//登入
struct Login
{
    char userName[32];
    char PassWoed[32];
};
struct LoginResult
{
    int result;
};
//退出
struct Logout
{
    char userName[32];
};
struct LogoutResult
{
    int result;
};


int main()
{
    //呼叫windows庫檔案
    //啟動Windows socket 2.x環境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);
    //1、建立一個socket 套接字
    SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //2、bind 繫結用於接受客戶端連線的 IP 埠
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);//繫結一個埠號
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");    //繫結到哪個ip地址
    if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
    {
        std::cout << "失敗" << std::endl;
    }
    else
    {
        std::cout << "成功" << std::endl;
    }

    //3、listen 監聽網路介面
    if (SOCKET_ERROR == listen(_sock, 5))
    {
        std::cout << "失敗" << std::endl;
    }
    else
    {
        std::cout << "成功" << std::endl;
    }

    //4、accept 等待接受客戶端連線
    sockaddr_in clientAddr = {};
    int nAddrLen = sizeof(sockaddr_in);

    SOCKET _cSock = INVALID_SOCKET;
    char msgBuf[] = "Hello, i m server.";

    _cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
    if (INVALID_SOCKET == _cSock) {
        std::cout << "失敗" << std::endl;
    }
    std::cout << "新加入客戶端IP=" << inet_ntoa(clientAddr.sin_addr) << std::endl;

    
    while (true)
    {
        DataHeader header = {};
        //5、接受客戶端的請求資料
        int nLen = recv(_cSock, (char*)&header, sizeof(DataHeader), 0);
        if (nLen <= 0)
        {
            std::cout << "客戶端已退出,任務結束。" << std::endl;
            break;
        }

        std::cout << "收到命令:" << header.cmd << "資料長度:" << header.dataLength << std::endl;
        
        switch (header.cmd)
        {
        case CMD_LOGIN:
        {
            Login login = {};
            recv(_cSock, (char*)&login, sizeof(Login), 0);
            //6、向客戶端傳送資料
            LoginResult ret = { 1 };
            send(_cSock, (char*)&header, sizeof(DataHeader), 0);
            send(_cSock, (char*)&ret, sizeof(LoginResult), 0);
        }
        break;
        case CMD_LOGOUT:
        {
            Logout logout = {};
            recv(_cSock, (char*)&logout, sizeof(logout), 0);
            //6、向客戶端傳送資料
            LogoutResult ret = { 1 };
            send(_cSock, (char*)&header, sizeof(header), 0);
            send(_cSock, (char*)&ret, sizeof(ret), 0);
        }
        break;
        default:
            header.cmd = CMD_ERROR;
            header.dataLength = 0;
            send(_cSock, (char*)&header, sizeof(header), 0);
            break;
        }
    }
    //7、closesocket關閉套接字
    closesocket(_sock);

    WSACleanup();
    std::cout << "已退出,任務結束。" << std::endl;

    system("pause");
    return 0;
}

客戶端:

#define WIN32_LEAN_AND_MEAN
#include<iostream>

//注意將WinSock2放在之前 否則會報錯
//或者使用巨集定義
#include<WinSock2.h>
#include<Windows.h>
//明確指定需要一個動態庫
//在工程中聯結器中加入lib動態庫
#pragma comment(lib,"ws2_32.lib")

enum CMD
{
    CMD_LOGIN,
    CMD_LOGOUT,
    CMD_ERROR
};
struct DataHeader
{
    short dataLength;
    short cmd;
};
//登入
struct Login
{
    char userName[32];
    char PassWoed[32];
};
struct LoginResult
{
    int result;
};
//退出
struct Logout
{
    char userName[32];
};
struct LogoutResult
{
    int result;
};
//網路資料報文的格式定義
//報文有兩個部分 包頭和包體
//包頭 描述本次訊息包的大小 描述資料的作用

int main()
{
    //呼叫windows庫檔案
    //啟動Windows socket 2.x環境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);

    //1、建立一個socket
    SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (INVALID_SOCKET == _sock)
    {
        std::cout << "建立socket失敗" << std::endl;
    }
    else
    {
        std::cout << "建立socket成功" << std::endl;
    }
    //2、連線伺服器
    sockaddr_in _sin = {};
    _sin.sin_family = AF_INET;
    _sin.sin_port = htons(4567);
    _sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
    if (SOCKET_ERROR == ret)
    {
        std::cout << "建立socket失敗" << std::endl;
    }
    else
    {
        std::cout << "建立socket成功" << std::endl;
    }
    while (true)
    {
        //3、輸入請求命令
        char cmdBuf[128] = {};
        std::cin >> cmdBuf;
        //4、處理請求
        if (0 == strcmp(cmdBuf, "exit"))
        {
            std::cout << "收到exit命令,任務結束。" << std::endl;
            break;
        }
        else if (0 == strcmp(cmdBuf, "login"))
        {
            Login login = { "xmq","xmq" };
            DataHeader dh = {sizeof(login),CMD_LOGIN};
            //5、向伺服器傳送命令
            send(_sock, (const char*)&dh, sizeof(dh), 0);
            send(_sock, (const char*)&login, sizeof(login), 0);
            //6、接受伺服器返回的資料
            DataHeader retHeader = {};
            LoginResult loginRet = {};
            recv(_sock, (char*)&retHeader, sizeof(retHeader), 0);
            recv(_sock, (char*)&loginRet, sizeof(loginRet), 0);
            std::cout << "LoginResult: " << loginRet.result<<std::endl;
        }
        else if (0 == strcmp(cmdBuf, "logout"))
        {
            Logout logout = { "xmq" };
            DataHeader dh = { sizeof(logout),CMD_LOGOUT };
            //5、向伺服器傳送命令
            send(_sock, (const char*)&dh, sizeof(dh), 0);
            send(_sock, (const char*)&logout, sizeof(logout), 0);
            //6、接受伺服器返回的資料
            DataHeader retHeader = {};
            LogoutResult logoutRet = {};
            recv(_sock, (char*)&retHeader, sizeof(retHeader), 0);
            recv(_sock, (char*)&logoutRet, sizeof(logoutRet), 0);
            std::cout << "LogoutResult: " << logoutRet.result << std::endl;
        }
        else
        {
            std::cout << "不支援命令,請重新輸入。" << std::endl;
        }
    }

    //7、closesocket 關閉套接字
    closesocket(_sock);

    WSACleanup();

    std::cout << "已退出,任務結束。" << std::endl;

    system("pause");
    return 0;
}