在Windows操作系统下,进程之间通信(IPC)可以通过多种机制实现,以下是一些常用的通信方法:
CreateFileMapping
和OpenFileMapping
函数,一个进程可以创建一块共享内存区域,其他进程通过相同的名称打开这个内存映射对象,从而实现对同一块内存的读写操作,达到数据共享的目的。在讨论网络中进程间的通信时,需要一种方式来唯一标识参与通信的进程,而TCP/IP协议栈为此提供了解决方案。
至于应用层的实现,套接字socket编程接口是目前最广泛使用的机制之一,它源自UNIX BSD系统,并且已经成为跨平台的网络编程标准。
可以说,“一切皆socket”
本文则将基于windows下的Socket编程构造一个简单的TCP回声服务端和客户端进行部分代码和TCP的原理的详解。
Socket,中文常译为“套接字”,是计算机网络中一个非常重要的概念,它是网络通信的基础之一。Socket 提供了一种跨网络通信的机制,允许两个不同计算机上的应用程序通过网络进行数据交换。在更具体的层面,Socket 可以被看作是网络上的两个程序通过一个双向通信链路进行对话的接口,有些人也将socket当成是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
Socket 的工作原理基于CS模型,其中一方扮演客户端角色,另一方扮演服务端角色。在Windows下大致流程如下:
首先,需要初始化网络库,如在Windows系统中使用WSAStartup函数初始化Winsock库,在Unix/Linux系统中通常不需要显式初始化。
#include <windows.h>
#include <iostream>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 0. 初始化网络环境
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("初始化Winsock失败\n");
return -1;
}
printf("初始化Winsock成功\n");
// 此处放置网络通信代码...
// 清理Winsock资源
WSACleanup();
printf("资源已清理\n");
return 0;
}
当你调用socket函数创建一个套接字(socket)时,它返回的套接字描述符唯一标识一个socket。这个socket描述字概念上类似于文件描述符,把它作为参数,通过它来进行一些数据传输操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
af:即协议域,又称为协议族(family)。常用的协议族有,AF_INET代表IPv4 AF_INET6代表IPv6等等。
type:指定socket类型。常用的socket类型有,SOCK_STREAM代表TCP连接,SOCK_DGRAM代表UDP等等
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP等,它们分别对应TCP传输协议、UDP传输协议
服务端和客户端程序都会调用socket函数创建一个Socket。这时需要指定通信的协议域、类型和指定协议(指定协议通常填0,让系统选择类型对应的默认协议)。
// 1. 创建服务端句柄(套接字)
// AF_INET ipv4 AF_INET6 ipv6
// SOCK_STREAM --> TCP SOCK_DREAM --> UDP
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockServer)
{
printf("创建服务端句柄失败\n");
WSACleanup();
return -1;
}
printf("1. 创建服务端成功\n");
当调用socket()函数创建套接字时,没有为该套接字分配具体的网络地址(IP地址和端口号)。要为套接字分配一个地址(主要是指IP地址和端口号),接下来登场的就是bind()函数
服务端想在其创建的Socket上绑定一个IP地址和端口号,需要调用bind()函数,并传入一个包含地址信息(如SOCKADDR_IN结构)的参数。这一步是将一个特定的网络地址与套接字关联起来,使得该套接字能够开始监听来自该地址的连接(针对服务端)或作为后续connect()调用的源地址(客户端)。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:这是通过之前socket()函数调用返回的套接字描述符。它是一个整数,代表了要绑定地址的套接字。此参数让操作系统知道你想要给哪个套接字分配地址信息。
addr:这是一个指向sockaddr结构的指针
并且struct sockaddr *类型的addr参数需要根据创建套接字时指定的协议域来具体化
对于IPv4,使用的结构体是struct sockaddr_in,它包含:
sin_family: 地址族成员,通常设置为AF_INET表示IPv4。
sin_port: 端口号,以网络字节序表示。
sin_addr: 包含IPv4地址的结构体,其成员s_addr存储32位的IPv4地址,同样采用网络字节序。
IPv6略
addrlen:这是一个socklen_t类型的值,表示addr所指向的地址结构的大小。
// 2. 绑定端口号和IP地址
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9870);// 端口号 host to net short
//addr.sin_addr.S_un.S_addr应当设置为服务端所在的IP地址
//addr.sin_addr.S_un.S_addr = INADDR_ANY; // IP地址 所有IP都行
//addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.4"); // 特定IP地址
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //本机地址
if (bind(sockServer, (sockaddr*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
printf("绑定端口号失败\n");
closesocket(sockServer);
WSACleanup();
return -1;
}
printf("2. 绑定端口号成功\n");
在调用socket()、bind()之后就该调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
服务端调用listen函数
int listen(int sockfd, int backlog);
开始监听绑定的端口上的连接请求。
sockfd:监听的socket
backlog:尚未被accept()调用接受的连接请求的最大数量,包括已完成三次握手但还未被服务器进程通过accept()处理的连接。这意味着,如果有大量快速到达的连接请求,超过这个数值的请求可能会被拒绝。
// 3. 监听端口号(告诉操作系统,与当前程序建立逻辑关联)
if (listen(sockServer, 5) == SOCKET_ERROR)
{
printf("监听端口号失败\n");
closesocket(sockServer);
WSACleanup();
return -1;
}
printf("3. 监听端口号成功\n");
TCP服务端通过依次调用socket()、bind()、listen()函数后,为指定的IP地址和端口配置并开始监听连接请求。具体来说:
socket()创建一个未绑定的套接字。
bind()将该套接字与一个特定的IP地址和端口号绑定。
listen()将套接字转换为监听模式,并设置等待连接队列的最大长度。
接下来应该使用connect()函数尝试与服务端的特定IP地址和端口建立连接。这个动作包含了TCP的三次握手过程,以建立可靠的连接。
服务端通过调用accept
函数接受一个来自客户端的连接请求,这将分配一个新的套接字描述符(socket)专门用于与这个客户端通信。原socket继续监听其他新的连接请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:这是服务器端通过之前调用socket()函数创建的套接字描述符,它代表了服务器正在监听的socket。(一个服务器通常通常仅仅只有一个监听socket描述字,它在该服务器的生命周期内一直存在。)
addr:这是一个指向struct sockaddr *的指针,用于接收客户端的地址信息。当accept()成功返回时,这个结构会被填充客户端的地址和端口信息,使得服务器知道是哪个客户端发起的连接。
addrlen:是指向socklen_t类型的指针,用于指定addr所指向的结构体的大小。在调用accept()前,需要先初始化这个值,以告知内核结构体的大小,调用后,一般会被更新以反映实际填写的地址结构的大小。
while (true)
{
// 4. 接收客户端连接 会建立一个新的套接字(他是客户端的标记)
printf("4. 准备等待客户端到来\n");
SOCKADDR_IN clientAddr = {};
int nAddrLen = sizeof(SOCKADDR_IN);
SOCKET sockClient = accept(sockServer, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == sockClient)
{
printf("接收客户端连接失败\n");
continue; // 错误处理后继续等待下一个客户端
}
printf("4. 接收客户端连接成功\n");
// 与客户端通信的循环
// 关闭客户端套接字
closesocket(sockClient);
printf("当前客户端已断开连接,等待下一个客户端...\n");
}
连接建立后,双方可以进行数据的发送和接收即实现了网咯中不同进程之间的通信!
读取数据:这通常使用recv()或read()函数从连接的套接字中读取数据。这些函数允许程序读取客户端或服务端发送的数据。
发送数据:同样地,它们可以使用send()或write()函数向对方发送数据。这些函数将数据写入套接字,进而传输到对方。
对于TCP连接,数据传输是基于流的,保证了数据的顺序和可靠性;而对于UDP,数据以数据报的形式发送,不保证顺序也不一定可靠。
对于Windows平台我一般使用recv和send函数进行I/O操作
int recv(
SOCKET s,
char FAR *buf,
int len,
int flags
);
s:套接字描述符,由之前创建套接字的socket()函数返回。它标识了进行数据读取的通信端点。
buf:指向缓冲区的指针,这个缓冲区用于接收数据。数据将被读入此缓冲区。
len:缓冲区的长度,以字节为单位。这个参数指定了最多可以从套接字中接收多少数据。
flags:控制接收操作的标志。常见的标志有MSG_PEEK(预览数据但不从接收队列中移除)等。如果不使用特殊功能,通常可以设置为0。
recv
函数的返回值有几种典型情况,每种都代表着不同的含义:
大于0的值:表示成功接收到了数据,返回值是实际接收到的字节数。这意味着数据从套接字缓冲区成功读取到了提供的缓冲区中。
等于0的值:这通常表示连接被对方关闭。在TCP连接中,当对端执行了正常的关闭流程(发送了FIN包),并且所有剩余数据都已被接收,recv可能返回0。这标志着数据传输的正常结束。
小于0的值:这表示发生了错误。在Windows系统中,错误值通常是SOCKET_ERROR(通常定义为-1)。此时,需要调用WSAGetLastError()来获取具体的错误代码,以便进一步分析错误原因,比如网络不可达、连接中断等问题。
当套接字被设置为非阻塞模式时,recv在没有数据可读的情况下也可能立即返回,此时返回值可能是WSAEWOULDBLOCK错误代码,表明调用应稍后再试而不应视为错误。此外,在某些情况下,如果接收操作被信号中断,recv也可能会返回-1,并且errno(在POSIX系统中)或WSAGetLastError()(在Windows中)可能设置为EINTR,表示操作被中断,需要重试。
int send(
SOCKET s,
const char FAR *buf,
int len,
int flags
);
s:同样是套接字描述符,标识了发送数据的通信端点。
buf:指向要发送数据的缓冲区的指针。这些数据将从这个缓冲区中读取并发送到连接的对端。
len:要发送的数据的长度,以字节为单位。
flags:与recv中的flags类似,用于控制发送操作的标志。常见的有MSG_OOB(发送带外数据)等。通常情况下,如果不需要特殊操作,可以设为0。
send函数的返回值同样有几种可能的情况,每种都代表特定的含义:
大于0的值:表示成功发送了数据,返回值是实际发送的字节数。这可能小于你试图发送的总字节数,特别是在设置了MSG_PARTIAL标志或操作被信号中断的情况下,但通常情况下应该等于你请求发送的字节数,除非发生错误或非阻塞模式下的特殊情况。
等于0的值:这种情况在TCP编程中是不常见的,通常表示没有数据被发送出去,这可能是因为套接字已被关闭或者出现了某些严重的错误。
小于0的值:表示发送操作失败。在Windows系统中,这通常是SOCKET_ERROR(值为-1)。此时,需要调用WSAGetLastError()来获取详细的错误代码,例如网络不可达、连接中断、缓冲区满等。
特别地,当套接字被设置为非阻塞模式时,如果发送缓冲区已满或者由于其他原因暂时无法发送更多数据,send可能立即返回SOCKET_ERROR并且WSAGetLastError()返回WSAEWOULDBLOCK,指示当前不能立即发送数据,应稍后再试。此外,如果发送操作被信号中断,在某些系统中,返回值也可能是-1,并且错误码指示为EINTR,同样需要处理并可能重试发送操作。
// 与客户端通信的循环
while (true)
{
char szData[1024] = {};
int ret = recv(sockClient, szData, sizeof(szData) - 1, 0);
if (ret > 0)
{
szData[ret] = '\0'; // 添加字符串结束符
printf("5. 接收客户端数据成功[%s]\n", szData);
// 发送回显数据
ret = send(sockClient, szData, ret, 0);
if (ret == SOCKET_ERROR)
{
printf("发送数据失败\n");
break; // 发送失败,断开与该客户端的连接
}
}
else if (ret == 0) // 客户端关闭连接
{
printf("客户端已主动断开连接。\n");
break; // 正常退出循环,准备处理下一个客户端
}
else // 发生错误
{
printf("接收客户端数据失败\n");
break; // 错误处理后断开连接
}
}
#include <windows.h>
#include <iostream>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 0. 初始化网络环境
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("初始化Winsock失败\n");
return -1;
}
printf("初始化Winsock成功\n");
// 1. 创建服务端句柄(套接字)
// AF_INET ipv4 AF_INET6 ipv6
// SOCK_STREAM --> TCP SOCK_DREAM --> UDP
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockServer)
{
printf("创建服务端句柄失败\n");
WSACleanup();
return -1;
}
printf("1. 创建服务端成功\n");
// 2. 绑定端口号和IP地址
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9870);// 端口号 host to net short
//addr.sin_addr.S_un.S_addr指明了服务端所在的主机位置。
//addr.sin_addr.S_un.S_addr应当设置为服务端所在的IP地址
//addr.sin_addr.S_un.S_addr = INADDR_ANY; // IP地址 所有IP都行
//addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.4"); // 特定IP地址
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //本机地址
if (bind(sockServer, (sockaddr*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
printf("绑定端口号失败\n");
closesocket(sockServer);
WSACleanup();
return -1;
}
printf("2. 绑定端口号成功\n");
// 3. 监听端口号(告诉操作系统,与当前程序建立逻辑关联)
if (listen(sockServer, 5) == SOCKET_ERROR)
{
printf("监听端口号失败\n");
closesocket(sockServer);
WSACleanup();
return -1;
}
printf("3. 监听端口号成功\n");
while (true)
{
// 4. 接收客户端连接 会建立一个新的套接字(他是客户端的标记)
printf("4. 准备等待客户端到来\n");
SOCKADDR_IN clientAddr = {};
int nAddrLen = sizeof(SOCKADDR_IN);
SOCKET sockClient = accept(sockServer, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == sockClient)
{
printf("接收客户端连接失败\n");
continue; // 错误处理后继续等待下一个客户端
}
printf("4. 接收客户端连接成功\n");
// 与客户端通信的循环
while (true)
{
char szData[1024] = {};
int ret = recv(sockClient, szData, sizeof(szData) - 1, 0);
if (ret > 0)
{
szData[ret] = '\0'; // 添加字符串结束符
printf("5. 接收客户端数据成功[%s]\n", szData);
// 发送回显数据
ret = send(sockClient, szData, ret, 0);
if (ret == SOCKET_ERROR)
{
printf("发送数据失败\n");
break; // 发送失败,断开与该客户端的连接
}
}
else if (ret == 0) // 客户端关闭连接
{
printf("客户端已主动断开连接。\n");
break; // 正常退出循环,准备处理下一个客户端
}
else // 发生错误
{
printf("接收客户端数据失败\n");
break; // 错误处理后断开连接
}
}
// 关闭客户端套接字
closesocket(sockClient);
printf("当前客户端已断开连接,等待下一个客户端...\n");
}
// 主循环结束后,关闭服务端套接字
closesocket(sockServer);
// 清理Winsock资源
WSACleanup();
printf("资源已清理\n");
return 0;
}
除了没有bind和listen以外,socket建立的流程与服务端相似,故略去
只讲讲connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:这是通过之前调用socket()函数创建的套接字描述符。
addr:这是一个指向struct sockaddr结构体的指针。sockaddr的介绍服务端有略
addrlen:是一个指针,指向存储地址结构大小的变量。
调用connect()函数后,它会尝试与指定地址的服务器建立相应连接。如果成功,函数会立即返回0。如果连接不能立即建立(例如,因为网络不可达或服务器未响应),函数会阻塞直到连接建立或超时/出错,此时返回-1,并且可以通过errno或WSAGetLastError()(在Windows下)获取具体的错误代码。
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9870);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // IP地址
int ret = connect(sockClient, (sockaddr*)&addr, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == ret)
{
printf("连接服务端失败\n");
return -1;
}
while (1)
#include <windows.h>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 0. 初始化网络环境
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("初始化Winsock失败\n");
return -1;
}
printf("初始化Winsock成功\n");
// 1. 创建客户端句柄(套接字)
// AF_INET ipv4
// SOCK_STREAM --> TCP SOCK_DREAM --> UDP
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockClient)
{
printf("创建客户端句柄失败\n");
return -1;
}
printf("1. 创建客户端句柄成功\n");
// 2. 连接服务端端口号和IP地址
SOCKADDR_IN addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9870);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // IP地址
int ret = connect(sockClient, (sockaddr*)&addr, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == ret)
{
printf("连接服务端失败\n");
return -1;
}
while (1)
{
char buf[1024] = { 0 };
printf("请输入字符:");
//从控制台一次读取一行
gets_s(buf);
// 3.发生数据给服务端
int retSend = send(sockClient, buf, strlen(buf), 0);
// retSend !=13 发送失败
// 4.接受服务端数据
char szRecv[4096] = {};
int retRecv = recv(sockClient, szRecv, 4096, 0);
if (retRecv <= 0)
{
printf("接受服务端数据失败\n");
return -1;
}
printf("接受到服务端数据:%s\n", szRecv);
}
// 5.关闭客户端句柄
closesocket(sockClient);
return 0;
}
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手
来关闭一个连接。
三次握手的目的是保证双方互相之间建立了连接。
三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手
大致流程如下
第一次握手:
1.客户端将SYN标志位置为1
2.生成一个随机的32位的序号seq=J , 这个序号后边是可以携带数据(数据的大小)
第二次握手:
1.服务器端接收客户端的连接: ACK=1
2.服务器会回发一个确认序号: ack=客户端的序号 + 数据长度 + SYN/FIN(按一个字节算)
3.服务器端会向客户端发起连接请求: SYN=1
4.服务器会生成一个随机序号:seq = K
第三次握手:
1.客户单应答服务器的连接请求:ACK=1
2.客户端回复收到了服务器端的数据:ack=服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)
四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。
因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
大致流程如下
1.某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
2.另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
3.一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
4.接收到这个FIN的源发送端TCP对它进行确认。
当前该echoServer(回声服务端)只能处理一个客户端连接进来,多个客户端连接服务端时该怎么办呢?是否可以分割接收到的客户端字符串来识别,并对客户端进行该字符串对应的消息转发呢?如何广播信息给除了发送信息的客户端以外的客户端呢?(实现简单的聊天室)
参考文章:
Socket通信基础
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。