1. 客户端需要执行的步骤
1.1 打开网络库并且校验版本
在Windows平台使用Visual studio进行网络通信相关开发的时候,首先需要导入头文件和静态库。
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
导入头文件后,就可以使用网络库中的函数。首先需要打开网络库,根据指定的版本打开一个网络库。使用到的函数是WSAStartup()。
参数&返回值 | 作用 |
---|---|
wVersionRequired | 需要的网络库版本 |
lpWSAData | 返回的网络库版本的信息 |
int WSAStartup(
WORD wVersionRequired,
LPWSADATA lpWSAData
);
示例:
// 1. 打开网络库
WORD wVersion = MAKEWORD(2, 2);// 2.2的网络库版本
WSADATA dwVersionInfo;
int status = WSAStartup(wVersion, &dwVersionInfo);
if (status != 0)
{
printf("Open Lib failed \n");
WSACleanup();
return -1;
}
// 版本校验
if (HIBYTE(dwVersionInfo.wVersion)!= 2 || LOBYTE(dwVersionInfo.wVersion) != 2)
{
printf("Open Version is not match\n");
WSACleanup();
return -1;
}
1.2 创建一个服务器socket
在操作系统中进行网络开发的时候,系统底层将复杂的协议栈进行封装,封装完成后的结果就是一个socket。在进行网络通信的时候,就是调用这些创建好的socket。
创建socket的函数如下。
参数&返回值 | 作用 |
---|---|
af | 地址类型 : 常用的地址有IPV4 IPV6等地址,在一般情况下 为TCP地址 |
type | 套接字类型, 常用的套接字类型有基于流和数据报类型的 TCP 协议一般为SOCK_STREAM, UDP一般为SOCK_DGRAM |
protocol | 协议类型: TCP协议为 IPPROTO_TCP , UDP协议为IPPROTO_UDP 就是各种类型的数据包 |
socket s | 若创建成功则会返回一个创建好的socket, 否则会返回SOCK_ERROR |
SOCKET socket(
int af,
int type,
int protocol
);
示例:
// 创建一个Socket
// 网络通信的时候,需要使用到SOCKET,其中一个作为服务端,一个作为客户端
// 参数: 1. 地址类型 IPV4类型
// 2. 套接字类型 数据流类型
// 3. 协议的类型 TCP协议
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockServer == INVALID_SOCKET)
{
printf("SOCKET 错误\n");
WSACleanup();
return -1;
}
1.3 绑定地址与端口
对于一个TCP的服务端,需要将SOCKET其与对应的IP地址和端口号进行绑定。
参数&返回值 | 作用 |
---|---|
SOCKET s | 创建好的服务器socket |
const sockaddr *addr | 服务器的IP地址和端口号等 |
int namelen | 参数二的长度 |
return | 绑定成功会返回0, 绑定失败会返回SOCK_ERROR |
在进行IP地址和端口号的绑定的时候,首先需要指定服务端的IP地址和端口号。从参数中可以看到所需要的数据第二个参数就是sockaddr这个类型的指针。在实际使用中,一般会先使用sockaddr_in这个类型的结构体对IP地址进行赋值,然后再进行转换。
int bind(
SOCKET s,
const sockaddr *addr,
int namelen
);
实列:
// 绑定地址与端口,
// 地址就是IP地址,端口号就是具体的应用程序
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(0x8080);
serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 进行地址绑定的时候, SOCKADDR_IN和SOCKADDR 其实是一样的
// 使用SOCKADDR 的时候会比较简单,因此可以使用SOCKADDR_IN的类型的数据进行赋值,
// 然后将其强转为SOCKADDR类型
status = bind(sockServer, (sockaddr *) &serverAddr, sizeof(serverAddr));
if (status != 0)
{
printf("绑定错误\n");
// 关闭具体的socket
closesocket(sockServer);
WSACleanup();
return -1;
}
1.4 启动监听
监听的作用就是,将套接字置于正在侦听传入连接的状态,只有启动监听了,服务端才能开始去与客户端建立连接。
参数&返回值 | 作用 |
---|---|
SOCKET s | 创建好的服务器socket |
int backlog | 挂起连接队列的最大长度 一般填 SOMAXCONN 让系统自己判断需要连接的个数 |
return | 绑定成功会返回0, 绑定失败会返回SOCK_ERROR |
int WSAAPI listen(
SOCKET s,
int backlog
);
示例:
// 开始监听函数
// 将套接字置于侦听正在传入链接的状态,socket 就开始工作了
status = listen(sockServer, 2);
if (status != 0)
{
printf("listen failed \n");
closesocket(sockServer);
WSACleanup();
return -1;
}
1.5 接收客户端传来的连接
listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket,通过返回值返回客户端的socket。
参数&返回值 | 作用 |
---|---|
SOCKET s | 创建好的服务器socket |
sockaddr *addr | 客户端的IP地址和端口信息的结构体 |
int *addrlen | 参数二的长度 |
return | 若成功返回,则返回的就是客户端的socket, 若返回错误,就会返回INVALID_SOCKET |
SOCKET WSAAPI accept(
SOCKET s,
sockaddr *addr,
int *addrlen
);
示例:
// accept 函数,用于将客户端的链接进行接受,能够为系统提供客户端的
// socket 和其socket信息. 监听的作用listen监听客户端来的链接,accept
// 将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket,
// 通过返回值返回给我们客户端的socket
sockaddr_in clientAddr;
int clientAddrLen;
// accept 函数是阻塞的,会一直等到客户端链接才会继续执行
SOCKET sockClient = accept(sockServer, NULL, NULL);
if (sockClient == INVALID_SOCKET)
{
printf("SOCKET 接受错误\n");
closesocket(sockServer);
}
1.6 发送数据和接收数据
1.6.1 接收函数
数据的接收都是由协议本身做的,也就是socket的底层做的,系统会有一段缓冲区,存储着接收到的数据。接收函数的本质就是将协议FIFO当中的数据读取到应用程序当中。
参数&返回值 | 作用 |
---|---|
SOCKET s | 创建好的服务器socket |
char *buf | 用于接收数据的字符数组 |
int len, | 想要接收的长度 |
flags | 一般填零就可以 |
return | 执行成功会返回接收到的数据个数, 返回值为0 说明连接中断或者客户端下线, 执行失败会返回SOCK_ERROR |
recv函数是一个阻塞的函数,也就是说若要接收的数据长度比协议缓冲区中的数据要多的话,它会一直等到协议缓冲区中有足够的数据。
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
// 接受数据函数recv
char buf[4096];
status = recv(sockClient, buf, 4096, 0);
if (status == 0)
{
printf("链接中断、 客户端下线\n");
}
else if (status == SOCKET_ERROR)
{
printf("接受错误\n");
}
else
{
printf("receive bytes %d\n", status);
}
1.6.2 发送函数
参数&返回值 | 作用 |
---|---|
SOCKET s | 要发送的客户端socket |
char *buf | 用于发送数据的字符数组 |
int len, | 想要发送的长度 |
flags | 一般填零就可以 |
return | 执行成功会返回接收到的数据个数, 执行失败会返回SOCK_ERROR |
int WSAAPI send(
SOCKET s,
const char *buf,
int len,
int flags
);
// 发送函数send
status = send(sockClient, buf, 4096, 0);
if (status == SOCKET_ERROR)
{
printf("发送错误\n");
}
else
{
printf("send bytes %d\n", status);
}
2. TCP 客户端
TCP 客户端实现的功能相较于服务端要简单很多,唯一的不同就是客户端只需要去连接服务端的socket就可以了。在客户端连接到服务端的时候,连接的socket需要连接服务端的socket。
connect函数
参数&返回值 | 作用 |
---|---|
SOCKET s | 服务器的socket |
const sockaddr *name | 服务端的IP地址和端口号的结构体 |
int namelen, | 参数二的长度 |
return | 连接成功会返回0,失败会返回SOCK_ERROR |
int WSAAPI connect
(
SOCKET s,
const sockaddr *name,
int namelen
);
示例:
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(0x8080);
serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
status = connect(sockServer, (sockaddr *)&serverAddr, sizeof(serverAddr));
if (status == SOCKET_ERROR)
{
printf("链接错误\n");
closesocket(sockServer);
return -1;
}