概述
在网络编程中,客户端/服务器模型(即C/S模型)是一种常见的架构模式。在这种模式下,一个或多个客户端向服务器建立连接,并发送请求;服务器接受这些连接,并处理请求、返回响应。
在C/S模型中,客户端会主动发起与服务器的连接,发送请求,并接收服务器的响应。客户端可以是任何能够发起网络连接的设备或应用程序。服务器通常运行在网络中的固定位置,监听特定端口以接受来自客户端的连接请求。
TCP客户端
TCP客户端负责发起与服务器的连接,并发送请求和接收响应。TCP客户端主要包含以下五个步骤,分别为:创建套接字、连接到服务器、发送数据、接收数据、关闭套接字。
1、创建Socket。首先,我们需要创建一个新的Socket实例。这一步,是通过调用socket函数来完成的。
2、连接到服务器。我们需要指定服务器的IP地址和端口号,并尝试建立连接。这是通过设置sockaddr_in结构体,并调用connect函数来完成的。默认情况下,connect函数是阻塞的。也就是说,只有等客户端完全连接上服务器,或者发生了错误,connect函数才会返回。
3、发送数据。一旦连接成功,我们就可以通过这个套接字向服务器发送数据了。这一步,是通过send或write函数来实现的。
4、接收数据。除了向服务器发送数据,客户端还可以从服务器接收数据,因为TCP连接是双向的、全双工的。这一步,是通过recv或read函数来实现的。
5、关闭套接字。当通信完成后,应当关闭套接字以释放系统资源。
为了便于理解上面的步骤,我们可以参考下面的流程图。
根据上面的流程图,我们可以编写下面的示例代码。该示例代码是一个简易的的TCP客户端程序,用于连接本机的8888端口,并进行简单的文字通信。
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
// 初始化Winsock
int nRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (nRet != 0)
{
printf("WSAStartup failed: %d\n", nRet);
return 1;
}
SOCKET sockClient = INVALID_SOCKET;
do
{
// 创建套接字
sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockClient == INVALID_SOCKET)
{
printf("Create socket failed: %d\n", WSAGetLastError());
break;
}
// 连接到服务器
struct sockaddr_in serverAddr;
ZeroMemory(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
nRet = connect(sockClient, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (nRet == SOCKET_ERROR)
{
printf("Connect server failed: %d\n", WSAGetLastError());
break;
}
// 发送数据
char pszSendBuf[] = "Hello from client";
nRet = send(sockClient, pszSendBuf, (int)strlen(pszSendBuf), 0);
if (nRet == SOCKET_ERROR)
{
printf("Send data failed: %d\n", WSAGetLastError());
break;
}
// 接收数据
char pszRecvBuf[512] = { 0 };
int nRecvBufLen = sizeof(pszRecvBuf);
nRet = recv(sockClient, pszRecvBuf, nRecvBufLen, 0);
if (nRet > 0)
{
printf("Received data: %s\n", pszRecvBuf);
}
else if (nRet == 0)
{
printf("Connection closed\n");
break;
}
else
{
printf("Recv data failed: %d\n", WSAGetLastError());
break;
}
} while (false);
// 关闭套接字
if (sockClient != INVALID_SOCKET)
{
closesocket(sockClient);
sockClient = INVALID_SOCKET;
}
WSACleanup();
getchar();
return 0;
}
TCP服务器
在网络编程的C/S模型中,服务器是提供服务的一方。它通常负责处理来自多个客户端的请求,执行相应的业务逻辑,并将结果返回给客户端。TCP服务器主要包含以下八个步骤,分别为:创建服务套接字、绑定套接字、监听连接请求、接受连接请求、接收数据、发送数据、关闭数据套接字、关闭服务套接字。
1、创建服务套接字。首先,我们需要创建一个新的Socket实例作为服务套接字。这一步,是通过调用socket函数来完成的。
2、绑定套接字。创建并初始化一个sockaddr_in结构体,用于存储服务器的IP地址和端口号。然后,使用bind函数将套接字绑定到指定的IP地址和端口。
3、监听连接请求。使用listen函数使套接字进入监听状态,以便可以接受来自客户端的连接请求。
4、接受连接请求。accept函数用于从已连接的客户端队列中接受一个连接请求。当一个客户端尝试连接到服务器时,服务器会将这个连接请求放入一个队列中(由listen函数设置的最大长度控制)。服务器可以通过调用accept函数来接受这些连接请求,并创建一个新的数据套接字来与该客户端进行通信。
5、接收数据。使用recv或read函数通过已建立的数据套接字接收数据。
6、发送数据:使用send或write函数通过已建立的数据套接字发送数据。
7、关闭数据套接字。当不需要数据通信时,关闭客户端套接字。
8、关闭服务套接字。在服务器退出时,关闭服务套接字。
为了便于理解上面的步骤,我们可以参考下面的流程图。
根据上面的流程图,我们可以编写下面的示例代码。该示例代码是一个简易的的TCP服务器程序,用于监听端口8888,并处理客户端连接,以进行简单的文字通信。
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
// 初始化Winsock
WSADATA wsaData;
int nRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (nRet != 0)
{
printf("WSAStartup failed: %d\n", nRet);
return 1;
}
SOCKET serverSocket = INVALID_SOCKET;
SOCKET dataSocket = INVALID_SOCKET;
do
{
// 创建服务器套接字
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serverSocket == INVALID_SOCKET)
{
printf("Create socket failed: %d\n", WSAGetLastError());
break;
}
// 绑定套接字
struct sockaddr_in serverAddr;
ZeroMemory(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
nRet = bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (nRet == SOCKET_ERROR)
{
printf("Bind socket failed: %d\n", WSAGetLastError());
break;
}
// 监听连接请求
nRet = listen(serverSocket, 10);
if (nRet == SOCKET_ERROR)
{
printf("Listen failed: %d\n", WSAGetLastError());
break;
}
printf("Server listening on port %d ...\n", 8888);
// 接受连接请求
struct sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
dataSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &clientAddrSize);
if (dataSocket == INVALID_SOCKET)
{
printf("Accept failed: %d\n", WSAGetLastError());
break;
}
// 接收数据
char pRecvBuf[512] = { 0 };
int nRecvBufLen = sizeof(pRecvBuf);
nRet = recv(dataSocket, pRecvBuf, nRecvBufLen, 0);
if (nRet > 0)
{
printf("Received data: %s\n", pRecvBuf);
// 发送数据
const char *pszSendBuf = "Hello from server";
nRet = send(dataSocket, pszSendBuf, (int)strlen(pszSendBuf), 0);
if (nRet == SOCKET_ERROR)
{
break;
}
}
else if (nRet == 0)
{
printf("Connection closed\n");
break;
}
else
{
printf("Recv data failed: %d\n", WSAGetLastError());
break;
}
} while (false);
// 关闭数据通信套接字
if (dataSocket != INVALID_SOCKET)
{
closesocket(dataSocket);
dataSocket = INVALID_SOCKET;
}
// 关闭服务器套接字
if (serverSocket != INVALID_SOCKET)
{
closesocket(serverSocket);
serverSocket = INVALID_SOCKET;
}
WSACleanup();
getchar();
return 0;
}
注意事项
在开发C/S模型的客户端和服务器程序时,需要特别注意以下几点。
1、错误处理。在整个过程中,应该注意检查每个操作是否成功,并妥善处理可能发生的错误。
2、并发控制。如果客户端需要同时与多个服务器通信或者处理多条消息,可能需要考虑线程或异步IO等技术来提高效率。
3、安全性。对于敏感信息的传输,应采用安全协议,比如:TLS/SSL加密通信。