1. 程式人生 > >1.SDL2_net TCP伺服器端和客戶端的通訊

1.SDL2_net TCP伺服器端和客戶端的通訊

這幾天打算把以前做的遊戲嘗試加入區域網聯機,恰巧SDL提供了對應的庫,即SDL2_net。

1.安裝

我的系統是ubuntu,安裝相對簡單,下面一個命令即可:

sudo apt install libsdl2-net-dev

等待安裝完成即可。

如果使用的是window,可以去官網下載對應的連結庫

http://www.libsdl.org/projects/SDL_net/

下載dll或者原始碼。

2.連結標頭檔案和庫檔案

無論使用的是cmake、makefile、還是visual studio,都是需要確定對應的標頭檔案路徑和庫檔案路徑;我這裡使用的是cmake,cmake是對makefile的封裝,跨平臺。本次專案檔案路徑大致如下:

client資料夾包為客戶端,server資料夾為伺服器端。

cmake中並沒有SDL2相關庫的Find*.cmake,所以需要自己新增。FindSDL2*.cmake在ubuntu下是可用的,未測試過windows。

server/CMakeLists.txt

#工程所需最小版本號
cmake_minimum_required(VERSION 3.10)

project(server)

#設定搜尋路徑
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/../cmake")

#找到SDL2_net庫
find_package(SDL2 REQUIRED)
find_package(SDL2_net REQUIRED)

#新增對應的標頭檔案搜尋目錄
include_directories(${SDL2_NET_INCLUDE_DIR})
#生成可執行檔案
aux_source_directory(. SRC_LIST)
add_executable(main ${SRC_LIST})
#連結對應的函式庫
target_link_libraries(main 
        ${SDL2_NET_LIBRARY}
        ${SDL2_LIBRARY})
#設定生成路徑在源路徑下
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR})

需要注意的是,SDL2_net是可以獨立使用的,不過如果要是使用SDL_Delay()、SDL_Init()等函式,還是需要SDL.h和相關庫檔案的。

client資料夾的CMakeLists.txt和上述相似,這裡不再贅述。

3.TCP

TCP是一個可靠的連線,相對於UDP來說較慢,但是更加安全。伺服器和客戶端需要先建立連線,然後收發資料,之後約定一個結束標誌,斷開連線。本示例只能一個伺服器和一個客戶端。

4.Server 伺服器端

伺服器端相對來說要比客戶端複雜,伺服器可以 指定監聽的埠號,比如:./main 2000,表示監聽埠為2000的伺服器;也可以不指定,不指定則在當期程式碼中使用預設埠2000。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "SDL_net.h"

#define SIZE 1024
int main(int argc, char** argv)
{
        IPaddress ip; 
        TCPsocket server_socket, client_socket;
        bool quit = false;
        char buffer[SIZE];
        //預設埠
        Uint16 port = 2000;

        SDL_Init(0);

        if (argc < 2)
        {
                printf("the server wiil use the default port:%u\n", port);
        }
        else
                port = (Uint16)atoi(argv[1]);

        printf("the server will use port:%u\n", port);
        SDLNet_Init();
        //建立一個伺服器型別的IPaddress
        if (SDLNet_ResolveHost(&ip, NULL, port) != 0)
        {
                printf("SDLNet_ResolveHost: %s\n", SDLNet_GetError());
                return 1;
        }

這一部分主要是初始化網路庫和填充IPaddress結構體 。


①.SDLNet_Init()           SDLNet_Quit()

初始化網路庫,使用該庫函式前都得首先呼叫該函式。、

退出網路庫。

②.IPsocket

typedef struct {
    Uint32 host;            /* 32-bit IPv4 host address */
    Uint16 port;            /* 16-bit protocol port */
} IPaddress;

ip地址和對應的埠號,這個結構體主要是使用下面的函式進行屬性填充,並不需要預先設定。

int SDLNet_ResolveHost(IPaddress *address, const char *host, Uint16 port)
  • address 將要填充的IPaddress結構體。
  • host 如果是伺服器端,則host為NULL;如果為客戶端,則填寫的是域名或者IP地址。比如127.0.0.1或者onatkins.org。
  • port 如果是伺服器端,則表示為要監聽的埠號;如果是客戶端,則表示對應的伺服器所監聽的埠號。

這個函式的主要作用就是填充IPaddress結構體,如果成功則返回0,否則返回-1,如果失敗,則很可能是host錯誤。

③.TCPsocket

typedef struct _TCPsocket *TCPsocket;

直譯就是TCP套接字,其內部屬性不公開(即該結構體的定義不在標頭檔案中),主要用作TCP連線,為指標。


        //開啟一個TCP連線
        if ((server_socket = SDLNet_TCP_Open(&ip)) == nullptr)
        {
                printf("error:%s\n", SDLNet_GetError());
                return 2;
        }
        printf("create server success\n");

這一部分主要是通過對應的IPaddress來建立一個TCP套接字,上面也講過,TCPsocket為指標型別,所以判斷其是否生成使用的是server_socket == nullptr.


④.SDLNet_TCP_Open(IPaddress* ip)

TCPsocket SDLNet_TCP_Open(IPaddress *ip)

根據已經處理好的IPaddress變數來建立一個TCP套接字,官網wiki上說如果ip->host == INADDR_ANY,則僅僅使用port。這個INADDR_ANY是一個巨集,其定義如下:

#ifndef INADDR_ANY
#define INADDR_ANY      0x00000000
#endif

具體相關目前我還不太懂,初學,初學。


        while (!quit)
        {
                //存在TCP連線
                if ((client_socket = SDLNet_TCP_Accept(server_socket)) != nullptr)
                {
                        IPaddress* remoteIP = nullptr;

                        //獲取遠端ip
                        if ((remoteIP = SDLNet_TCP_GetPeerAddress(client_socket)) != nullptr)
                        {
                                printf("remote ip is %x, port %u\n"
                                        , SDLNet_Read32(&remoteIP->host)
                                        , SDLNet_Read16(&remoteIP->port));
                        }
                        bool running = true;

                        //監聽
                        while (running)
                        {
                                if (SDLNet_TCP_Recv(client_socket, buffer, SIZE) > 0)
                                {
                                        printf("Client say: %s\n", buffer);

                                        if (strcmp(buffer, "exit") == 0)
                                        {
                                                running = false;
                                                printf("Terminal\n");
                                        }
                                        else if (strcmp(buffer, "quit") == 0)
                                        {
                                                quit = true;
                                                running = false;
                                                printf("Quit\n");
                                        }
                                }
                        }
                        SDLNet_TCP_Close(client_socket);
                }
        }

這一部分主要負責監聽是否存在客戶端,如果是的話,則建立連線,然後等待對方發訊息。


⑤.SDLNet_TCP_Accept()

TCPsocket SDLNet_TCP_Accept(TCPsocket server)

接收一個server對應的即將到來的客戶端連線,非阻塞,如果沒有返回NULL。不要用這個函式用於已經連線的伺服器(試了試,當存在多個客戶端時,僅僅第一個客戶端有效,第二個客戶端傳送的資料,伺服器端接收不到,當然,也和上面的程式碼相關)。

⑥.SDLNet_TCP_Recv()

int SDLNet_TCP_Recv(TCPsocket sock, void *data, int maxlen)
  • sock 合法的、已連線的客戶端套接字。
  • data 要存放的資料,需要注意的是為void*。
  • maxlen 接收的資料的最大長度。
  • return 如果返回值小於等於0,則發生錯誤,可能是客戶端已經斷開連線。

 

 5.客戶端

客戶端做的事非常類似與伺服器,這裡負責傳送資料。

#include <string.h>
#include <stdlib.h>
#include "SDL_net.h"

#define SIZE 1024
int main(int argc, char** argv)
{
        IPaddress ip;
        TCPsocket server_socket;
        bool quit = false;
        int len = 0;
        char buffer[1024];

        if (argc < 3)
        {
                printf("Usage: %s host port\n", argv[0]);
                return 1;
        }
        SDLNet_Init();
        //客戶端
        if (SDLNet_ResolveHost(&ip, argv[1], atoi(argv[2])) != 0)
        {
                printf("error:%s\n", SDLNet_GetError());
                return 1;
        }
        //開啟ip
        if ((server_socket = SDLNet_TCP_Open(&ip)) == nullptr)
        {
                printf("error:%s\n", SDLNet_GetError());
                return 1;
        }

        while (!quit)
        {
                printf("Write something:\n");
                scanf("%s", buffer);

                len = strlen(buffer) + 1;

                if (SDLNet_TCP_Send(server_socket, (void*)buffer, len) < len)
                {
                        printf("error:%s\n", SDLNet_GetError());
                }
                if (strcmp(buffer, "exit") == 0 || strcmp(buffer, "quit") == 0)
                        quit = true;
        }

        SDLNet_TCP_Close(server_socket);
        SDLNet_Quit();
        return 0;
}

傳送訊息給伺服器。


⑦.SDLNet_TCP_Send()

int SDLNet_TCP_Send(TCPsocket sock, const void *data, int len)
  • sock 合法的、已連線的TCP套接字。
  • data 要傳送的資料。
  • len 傳送的資料的長度。
  • return 傳送的資料長度,小於等於len

 

 6.執行

伺服器和客戶端編寫完成,編譯好執行即可。

伺服器執行

客戶端執行,並嘗試傳送資料1234給伺服器

伺服器接收,並輸出:

客戶端可主動申請傳送exit釋放此次連線或傳送quit退出,這裡不再演示。

本節程式碼如下:

https://github.com/sky94520/SDL2_net

區域網:

伺服器端:ubuntu 客戶端:ubuntu (由於SDL的跨平臺性,其他的系統應該也可以連線成功)

測試成功

雲伺服器+ubuntu客戶端

測試失敗


該程式測試在區域網下成功,而在雲伺服器上失敗,估計是雲伺服器還另外需要其他的程式支撐,,,暫時無法解決。。。