这几天打算把以前做的游戏尝试加入局域网联机,恰巧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客户端
测试失败
该程序测试在局域网下成功,而在云服务器上失败,估计是云服务器还另外需要其他的程序支撑,,,暂时无法解决。。。