Socket 编程
基本知识
socket 编程有三种,流式套接字 SOCK_STREAM ,数据报套接字 SOCK_DGRAM ,原始套接字 SOCK_RAW ,前两者较常用。基于 TCP 的 socket 编程是流式套接字。
Socket(套接字):一种应用程序接口 API
TCP:建立在 IP 之上,通过握手建立连接,对每个 IP 包编号,确保对方按顺序收到,如果包丢失,就自动重发
UDP:建立在 IP 之上,但面向无连接的通信协议,不能保证数据包的顺利到达,是不可靠传输,但传输效率比 TCP 高
WinSocket 包含 Socket 和 Socket2 ,一般使用 Socket2 ,在使用时需要包含如下头文件和 LIB 文件
#include <WinSock.h> //一般导入下面那个
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
使用 g++ 编译 Socket 程序时,要在“工具-编译选项”中加入 -lwsock32 参数,与窗口程序同时使用按如下格式:
-mwindows -lwsock32
既然涉及到 IP 地址,我们需要掌握相关的表现形式。在命令行中输入 ipconfig 查看本机 ip 地址
ipconfig
IP 地址常用点分法表示,如 192.168.0.169 ,即 4 个 0-255 的整数表示,计算机中不使用这种方法保存 IP ,因为浪费存储空间且不利于计算,因此采用无符号长整型数储存 IP 地址。
如果我们需要保存和传递 IP 地址,就要按照特定的形式进行转换。在网络传输中 IP 地址保存为 32 位二进制,在低位存储中保存数据的高位字节。这种存储顺序格式被称为网络字节顺序,按照 32 位二进制数为一组进行传输,数据传输顺序由高到低。在 VC 中使用 in_addr 类型来保存 IP 地址,另外还有转换函数
// IP 转地址字符串
char FAR * inet_ntoa(
struct in_addr in
);
// 地址字符串转 IP
unsigned long inet_addr(
const char FAR * cp
);
例如
in_addr addr;
addr.s_addr = inet_addr("198.168.0.12");
不同主机对IP地址的存储格式不同,需要通过以下函数实现主机和网络字节顺序的互转(host to network u_long):
// 将主机字节顺序格式 IP 转为 TCP/IP 网络字节顺序
u_long htonl(
u_long hostlong
);
// 主机转网络 u_short 型
u_short htons(
u_short hostshort
);
// 网络转主机 long
u_long ntohl(
u_long netlong
);
// 网络转主机 short
u_short ntohs(
u_short netshort
);
结构变量
首先创建版本号
WORD wVersion = MAKEWORD(2, 2); // Socket2 版本
还需要初始化 WinSocket
Int WSAStartup(
WORD wVersionRequested, //版本号
LPWSADATA lpWSAData
);
其中 WSAData 结构用于存放返回的 Socket 数据,它有结构
typedef struct WSAData {
WORD wVersion; // 期望用户使用的规范版本
WORD wHighVersion; // 可支持的最高版本
char szDescription[WSADESCRIPTION_LEN+1]; // 描述字符串
char szSystemStatus[WSASYS_STATUS_LEN+1]; // 状态字符串
unsigned short iMaxSockets; // 套接字最大编号
unsigned short iMaxUdpDg; // 忽略
char FAR * lpVendorInfo; // 废弃
} WSADATA, FAR * LPWSADATA;
如果返回 0 表示初始化成功
创建 Socket 变量的函数
SOCKET socket(
int af, // 套接字地址
int type, // 套接字类型(此处为流式套接字 TCP )
int protocol // 使用协议(TCP)
);
于是我们定义服务端的 Socket 变量
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer==INVALID_SOCKET)
{
// 初始化失败
}
其中 PF_INET 和 AF_INET 是等价的,为了安全起见,需要判断初始化是否成功再进行操作
建立连接还需要提供接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET; // 设置端口地址
addrServer.sin_port = htons(8888); // 网络字节
addrServer.sin_addr.s_addr = htonl(INADDR_ANY); // 主机 IP
特别的,'127.0.0.1'
表示本机 IP 地址, 8888 为端口号, INADDR_ANY 表示不指定 ip
注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定
使用 bind 函数绑定端口到本地地址
int bind(
SOCKET s, // 套接字
const struct sockaddr FAR * name, // 端口名称
int namelen // 名称长度
);
例如我们建立服务端绑定
int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet==SOCKET_ERROR)
{
// 绑定失败,释放套接字
closesocket(hServer);
WSACleanup();
}
通过 listen 函数进行数据监听
int listen(
SOCKET s, // 套接字
int backlog // 最大连接数
);
例如设置最大监听数为 1
nRet = listen(hServer, 1); //最大连接数设为1
if (nRet==SOCKET_ERROR)
{
// 绑定失败,释放套接字
closesocket(hServer);
WSACleanup();
}
服务端通过 accept 获取客户端连接请求
SOCKET accept(
SOCKET s, // 套接字
struct sockaddr FAR * addr, // 端口地址
int FAR * addrlen // 地址长度
);
我们需要先为客户端也定义套接字,然后进行连接
SOCKET hClient; // 用户套接字
sockaddr_in addrClient; // 用户结构
int nLen = sizeof(addrClient); // 地址长度
hClient = accept(hServer, (sockaddr *)&addrServer, &nLen); // 接收客户端信息
if (hClient==INVALID_SOCKET)
{
// 接收失败
closesocket(hServer);
WSACleanup();
}
而对于客户端,需要通过 connect 连接服务端
int connect(
SOCKET s, // 套接字
const struct sockaddr FAR * name, // 端口名
int namelen // 长度
);
通过 send 函数向对方发送数据
int send(
SOCKET s, // 套接字
const char FAR * buf, // 发送数据
int len, // 数据长度
int flags // 风格
);
最后就是循环接收客户端数据,需要重复调用 recv 函数
int recv(
SOCKET s, // 套接字
char FAR * buf, // 接收缓冲区
int len, // 缓冲区长度
int flags // 风格
);
我们给出一个示例:
char szBuf[255]; // 定义缓存区
while (TRUE)
{
memset(szBuf, 0, sizeof(char)*255); // 清空缓存区
nRet = recv(hClient, szBuf, sizeof(char)*255, 0); // 接收数据
if (nRet==SOCKET_ERROR)
{
// 接收失败,关闭连接
closesocket(hClient);
closesocket(hServer);
WSACleanup();
}
}
两端连接
对于服务端,我们分成几个块来实现这几个流程,注意它们中有些部分是可以依次执行的,但是建议将每一部分拆成一个函数来使用。需要的前置声明:
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <cstdlib>
#include <iomanip>
#include <iostream>
#define BUFSIZE 255
客户端始终是一样的,简短且不需要分块;复杂的操作和阻塞式区别出现在服务端,因此只需关注服务端的分块流程
服务端
建立socket,申明自身的 port 和 IP ,并绑定到 socket ,使用 listen 监听,然后不断用 accept 去查看是否有连接。如果有,捕获 socket ,并通过 recv 获取消息的内容,通信完成后调用 closeSocket 关闭这个对应 accept 到的 socket ,如果不需要等待任何客户端连接,那么用 closeSocket 直接关闭自身的 socket
初始化过程:
// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
std::cout << "SOCKET初始化" << std::endl;
}
else
{
std::cout << "初始化失败" << std::endl;
}
定义端口结构,绑定本地地址,这一部分作为函数要返回服务器接口
// 服务端接口
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
std::cout << "初始化失败" << std::endl;
}
else
{
std::cout << "套接字初始化成功!" << std::endl;
}
// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定
// 绑定本地地址( nRet 获取返回值)
int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet == SOCKET_ERROR)
{
// 绑定失败,释放套接字
std::cout << "绑定失败!" << std::endl;
closesocket(hServer);
WSACleanup();
}
else
{
std::cout << "绑定套接字成功!" << std::endl;
}
// 数据监听
nRet = listen(hServer, 1); // 最大连接数设为 1
if (nRet == SOCKET_ERROR)
{
// 绑定失败,释放套接字
std::cout << "监听失败!" << std::endl;
closesocket(hServer);
WSACleanup();
}
else
{
std::cout << "开始监听..." << std::endl;
}
获取客户端请求
// 获取客户端请求
SOCKET hClient; // 用户套接字
sockaddr_in addrClient; // 用户结构
int nLen = sizeof(addrClient);
hClient = accept(hServer, (sockaddr *)&addrServer, &nLen); // 接收客户端信息
if (hClient == INVALID_SOCKET)
{
// 接收失败
std::cout << "接收失败,连接中断!" << std::endl;
closesocket(hServer);
closesocket(hClient);
WSACleanup();
}
else
{
std::cout << "连接客户端成功!" << std::endl;
}
std::cout << "连接完成,开始数据传输..." << std::endl;
循环接收客户端数据
// 循环接收客户端数据
char recvBuf[BUFSIZE];
char sendBuf[BUFSIZE];
while (TRUE)
{
nRet = recv(hClient, recvBuf, sizeof(char) * BUFSIZE, 0);
if (nRet == SOCKET_ERROR)
{
// 接收失败
std::cout << "接收失败,连接中断!" << std::endl;
break;
}
std::cout << "客户端消息:" << recvBuf << std::endl;
if (strcmp(recvBuf, "close") == 0)
{
nRet = send(hClient, "close", strlen("close"), 0);
std::cout << "连接中断" << std::endl;
break;
}
else
{
std::cout << "回复消息:";
std::cin >> sendBuf;
nRet = send(hClient, sendBuf, strlen(sendBuf), 0);
if (nRet == SOCKET_ERROR)
{
std::cout << "传输出错,连接中断" << std::endl;
break;
}
}
// 清空缓冲区
memset(recvBuf, 0, sizeof(char) * BUFSIZE);
memset(sendBuf, 0, sizeof(char) * BUFSIZE);
}
释放套接字
// 释放 Socket
closesocket(hClient);
closesocket(hServer);
WSACleanup();
客户端
建立 socket ,通过端口号和地址确定目标服务器,使用 Connect 连接到服务器, send 发送消息,等待处理,通信完成后调用 closeSocket 关闭 socket
int main()
{
// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
std::cout << "SOCKET初始化" << std::endl;
}
else
{
std::cout << "初始化失败" << std::endl;
}
// 服务端接口
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
std::cout << "初始化失败" << std::endl;
}
else
{
std::cout << "套接字初始化成功!" << std::endl;
}
// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定到目标 IP 地址
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定
// 连接服务器( nRet 获取返回值)
int nRet = connect(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet == SOCKET_ERROR)
{
// 连接失败,释放套接字
std::cout << "连接失败!" << std::endl;
closesocket(hServer);
WSACleanup();
}
else
{
std::cout << "连接服务器成功!" << std::endl;
}
// 向服务器端发送请求
char recvBuf[BUFSIZE];
char sendBuf[BUFSIZE];
while (TRUE)
{
std::cout << "发送消息:";
std::cin >> sendBuf;
nRet = send(hServer, sendBuf, strlen(sendBuf), 0);
if (nRet == SOCKET_ERROR)
{
std::cout << "传输出错,连接中断" << std::endl;
break;
}
// 接收消息
nRet = recv(hServer, recvBuf, sizeof(char) * BUFSIZE, 0);
if (nRet == SOCKET_ERROR)
{
// 接收失败
std::cout << "接收失败,连接中断!" << std::endl;
break;
}
if (strcmp(recvBuf, "close") == 0)
{
nRet = send(hServer, "close", strlen("close"), 0);
std::cout << "连接中断" << std::endl;
break;
}
else
{
std::cout << "服务端消息:" << recvBuf << std::endl;
}
// 清空缓冲区
memset(recvBuf, 0, sizeof(char) * BUFSIZE);
memset(sendBuf, 0, sizeof(char) * BUFSIZE);
}
// 释放 Socket
closesocket(hServer);
WSACleanup();
std::system("pause");
return 0;
}
非阻塞式
在创建一个套接字后,默认是阻塞式的, WinSocket 中的 I/O 函数: Send 和 Recv 操作必须等待完成相应的 I/O 操作后才能继续执行,每次只服务一个连接,只有当当前客户端连接断开后才会接入下一个客户端,因此阻塞式并发连接模式需要通过多线程同时服务多个连接,关于多线程的操作在另一个文档中记录。
相对的,无论操作是否完成,非阻塞式函数立即返回,它采用的是轮流监听的模式。我们可以调用
int ioctlsocket(
SOCKET s, // 套接字
long cmd, // 命令
u_long FAR * argp // 标记套接字模式
);
函数改变套接字模式,例如将套接字设为非阻塞式
u_long nNoBlock = 1;
ioctlsocket(s, FIONBIO, (u_long FAR*)&nNoBlock);
除此之外的流程都较为类似
初始化过程:
// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
std::cout << "SOCKET初始化" << std::endl;
}
else
{
std::cout << "初始化失败" << std::endl;
}
定义端口结构,绑定本地地址,这一部分作为函数要返回服务器接口
// 服务端接口
SOCKET hServer;
hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
std::cout << "初始化失败" << std::endl;
}
else
{
std::cout << "套接字初始化成功!" << std::endl;
}
// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定
// 绑定本地地址( nRet 获取返回值)
int nRet = bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer));
if (nRet == SOCKET_ERROR)
{
// 绑定失败,释放套接字
std::cout << "绑定失败!" << std::endl;
closesocket(hServer);
WSACleanup();
}
else
{
std::cout << "绑定套接字成功!" << std::endl;
}
// 数据监听
nRet = listen(hServer, 1); // 最大连接数设为 1
if (nRet == SOCKET_ERROR)
{
// 绑定失败,释放套接字
std::cout << "监听失败!" << std::endl;
closesocket(hServer);
WSACleanup();
}
else
{
std::cout << "开始监听..." << std::endl;
}
设置非阻塞模式,获取客户端请求,这里的代码表示持续等待直到连接到客户端为止
// 设置非阻塞模式
int iMode = 1; // 为 1 表示非阻塞, 0 表示阻塞
// 第二个类型为 FIONBIO 可用
nRet = ioctlsocket(hSocket, FIONBIO, (u_long FAR *)&iMode); //设为非阻塞式
// 创建客户端套接字
SOCKET hClient; //用户套接字
sockaddr_in addrClient; //用户端口
int nLen = sizeof(addrClient);
while (true)
{
// 由于是非阻塞式,该函数会立即返回
hClient = accept(hServer, (sockaddr *)&addrServer, &nLen); // 连接客户端
// 因此要判断返回套接字是否是由于未接收到客户端而无效
if (hClient == INVALID_SOCKET)
{
int nErrorCode = WSAGetLastError();
// 如果是未接受到客户端,则暂停然后继续循环
if (nErrorCode == WSAEWOULDBLOCK)
{
// 连接请求不能立即执行,先休眠然后继续
Sleep(100);
continue;
}
else
{
// 否则出现错误
closesocket(hSocket);
WSACleanup();
break;
}
}
}
收发消息
// 收发消息
while (true)
{
// 初始化缓存区
memset(Buf, 0, BUF_SIZE);
nRet = recv(hClient, Buf, BUF_SIZE, 0);
// 同样判断立即返回的信息是否是尚未接收数据造成的错误
if (nRet == SOCKET_ERROR)
{
int nErrorCode = WSAGetLastError();
if (nErrorCode == WSAEWOULDBLOCK)
{
// 缓存区暂无数据,先休眠然后继续
Sleep(100);
continue;
}
else if (nErrorCode == WSAETIMEDOUT || nErrorCode == WSAENETDOWN) // 超时或网络错误
{
closesocket(hSocket);
closesocket(hClient);
WSACleanup();
break;
}
}
// 返回消息时同理省略 ...
}
释放套接字
// 释放 Socket
closesocket(hServer);
closesocket(hServer);
WSACleanup();
多机连接
通常我们并不只建立两个主机之间的连接,服务端需要同时处理多个客户端传来的数据,因此我们在下面给出一个服务端创建的范例。客户端只需要建立与服务端的连接,因此不需要修改。
首先列出所有需要的函数的声明:
bool init(); // 初始化 WSASocket
void debugLog(std::string debug); // 调试错误输出
void debugTip(std::string tip); // 运行提示
SOCKET bindListen(int linkNum); // 建立服务端端口,绑定本地地址,开启监听,返回服务器端套接字
SOCKET buildConnection(SOCKET hServer); // 连接一个客户端,返回客户端套接字
bool clientConnect(SOCKET hClient); // 处理一个客户端连接进行收发数据
DWORD WINAPI clientThread(LPVOID lpParam); // 客户端线程
bool closeConnect(SOCKET hClient); // 关闭一个连接
void myTCP(); // 服务器主体
主函数是非常简单的,我们把复杂操作全部放到了函数中处理
int main()
{
init(); // 初始化
myTCP(); // 数据处理
WSACleanup(); // 释放 WinSocket
std::system("pause");
return 0;
}
两个调试用的函数定义
// 调试输出错误
void debugLog(std::string debug)
{
std::cout << debug << ",错误:" << WSAGetLastError() << std::endl;
}
// 运行提示
void debugTip(std::string tip)
{
std::cout << "系统消息:" << tip << std::endl;
}
初始化过程
bool init()
{
// 创建版本号
WORD wVersion = MAKEWORD(2, 2);
// 创建 WSADATA 结构
WSADATA wsaData;
// 初始化 WinSocket
if (WSAStartup(wVersion, &wsaData) == 0)
{
debugTip("Socket初始化成功");
return true;
}
else
{
debugLog("Socket初始化失败");
return false;
}
}
完成初始化后,可以看到使用 myTCP 函数,这个函数集成了多个步骤,监听过程和接收处理都在其中
void myTCP()
{
SOCKET hSocket = bindListen(1);
if (hSocket == INVALID_SOCKET)
{
debugLog("服务器初始化失败");
return;
}
while (true)
{
// 返回客户端套接字
SOCKET hClient = buildConnection(hSocket);
if (hClient == INVALID_SOCKET)
{
debugLog("客户端连接失败");
break;
}
// 当接收到客户端连接请求就为他开一个线程
DWORD dwThreadId;
HANDLE hThread = CreateThread(0, 0, clientThread, (LPVOID)hClient, 0, &dwThreadId);
if (hThread)
{
// 线程退出后释放句柄,防止句柄占用导致程序崩溃
CloseHandle(hThread);
}
// // 处理客户端进程
// if (clientConnect(hClient)==false) {
// debugLog("建立进程失败");
// }
// // 关闭一个客户端连接
// if (closeConnect(hClient)==false) {
// debugLog("关闭客户端失败");
// }
}
if (closesocket(hSocket) == SOCKET_ERROR)
{
debugLog("关闭socket失败");
}
else
{
debugTip("socket关闭");
}
}
之所以采用这种形式而非前面那种流程,是因为要建立多个连接,就必须循环建立客户端连接,每次都检测是否还有可连接的客户端,因此需要频繁调用 buildConnection 函数;注意其中注释掉的部分,那原本是用于处理客户端数据和关闭客户端的部分,但是为了能够同时处理多个客户端的进程,我们采用了多线程函数 clientThread ,它在前面通过 CreateThread 构建,因此取代了之前的处理方法。
当然,在最开始,还需要调用 bindListen 函数,它用于建立服务端端口,绑定本地地址,开启监听,返回服务器端套接字
SOCKET bindListen(int linkNum)
{
// 服务端接口
SOCKET hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hServer == INVALID_SOCKET)
{
debugLog("服务器端口初始化失败");
return INVALID_SOCKET;
}
else
{
debugTip("服务器端口初始化成功...");
}
// 接收端口
sockaddr_in addrServer; // 定义端口结构
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8888);
addrServer.sin_addr.s_addr = htonl(INADDR_ANY);
// '127.0.0.1' 表示本机 IP 地址,8888 为端口号,INADDR_ANY 表示不指定 ip
// 注意:非标准服务使用 8888 端口号,小于 1024 的端口号必须要有管理员权限才能绑定
// 绑定本地地址
if (bind(hServer, (sockaddr *)&addrServer, sizeof(addrServer)) == SOCKET_ERROR)
{
// 绑定失败,释放套接字
debugLog("绑定失败");
closesocket(hServer);
WSACleanup();
return INVALID_SOCKET;
}
else
{
debugTip("绑定成功...");
}
// 数据监听,最大连接数设为 linkNum
if (listen(hServer, linkNum) == SOCKET_ERROR)
{
// 绑定失败,释放套接字
debugLog("监听失败");
closesocket(hServer);
WSACleanup();
return INVALID_SOCKET;
}
else
{
debugTip("开始监听...");
}
return hServer;
}
需要循环调用的连接客户端,返回客户端套接字的函数
SOCKET buildConnection(SOCKET hServer)
{
// 获取客户端请求
sockaddr_in addrClient; // 客户端结构
int nLen = sizeof(addrClient);
SOCKET hClient = accept(hServer, (sockaddr *)&addrClient, &nLen); // 接收客户端连接
if (hClient == INVALID_SOCKET)
{
// 接收失败
debugLog("接收失败,连接中断!");
closesocket(hServer);
closesocket(hClient);
WSACleanup();
return INVALID_SOCKET;
}
else
{
debugTip("连接客户端成功!");
}
debugTip("连接完成,开始数据传输...");
return hClient;
}
由于存在多个连接,我们使用多线程处理
// 线程处理逻辑
DWORD WINAPI clientThread(LPVOID lpParam)
{
SOCKET hClient = (SOCKET)lpParam;
// 处理客户端进程
if (clientConnect(hClient) == false)
{
debugTip("进程退出");
}
// 关闭一个客户端连接
if (closeConnect(hClient) == false)
{
debugTip("客户端关闭");
}
return 0;
}
在该函数中调用处理和关闭客户端的函数:在循环中处理一个客户端连接,进行收发数据
bool clientConnect(SOCKET hClient)
{
// 循环接收客户端数据
char recvBuf[BUFSIZE];
char sendBuf[BUFSIZE];
while (TRUE)
{
// SOCKET_ERROR == 0
if (recv(hClient, recvBuf, BUFSIZE, 0) == SOCKET_ERROR)
{
// 接收失败
debugLog("接收失败,连接中断");
return false;
}
std::cout << "客户端消息:" << recvBuf << std::endl;
if (strcmp(recvBuf, "close") == 0)
{
send(hClient, "close", strlen("close"), 0);
debugTip("连接中断");
return false;
}
else
{
std::cout << "回复消息:";
std::cin >> sendBuf;
if (send(hClient, sendBuf, strlen(sendBuf), 0) == SOCKET_ERROR)
{
debugTip("传输出错,连接中断");
return false;
}
}
// 清空缓冲区
memset(recvBuf, 0, BUFSIZE);
memset(sendBuf, 0, BUFSIZE);
}
return true;
}
关闭客户端连接的函数
// 关闭一个连接
bool closeConnect(SOCKET hClient)
{
// 首先发送一个 TCP FIN 分段,向对方表明已经完成数据发送
if (shutdown(hClient, SD_SEND) == SOCKET_ERROR)
{
debugLog("连接关闭失败");
return false;
}
char Buf[BUFSIZE];
int nRetByte;
do
{
nRetByte = recv(hClient, Buf, BUFSIZE, 0);
if (nRetByte == SOCKET_ERROR)
{
debugLog("接收失败");
return false;
}
else if (nRetByte > 0)
{
debugTip("最后的接收数据");
}
} while (nRetByte != 0);
if (closesocket(hClient) == SOCKET_ERROR)
{
debugLog("socket关闭失败");
return false;
}
return true;
}