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客戶端
測試失敗
該程式測試在區域網下成功,而在雲伺服器上失敗,估計是雲伺服器還另外需要其他的程式支撐,,,暫時無法解決。。。