网络编程——套接字(Socket)通讯具体流程详解
注意:本文代码部分使用C语言进行描述,不同的编程语言在细节上可能存在差异
简介
套接字
Socket 的原意是“插座”,在计算机通信领域,Socket 被翻译为“套接字”。其命名寓意着Socket协议可以像插座一样即插即用,快速联通网络上的两台电脑。套接字作为TCP的上层协议,可以使用户十分轻易地在计算机网络中互相传递消息,而无需过多关注复杂的TCP以及IP协议。其中,使用文件描述 fd 来标记套接字对象。
文件描述符
文件描述符 fd (File descriptor) 是一个非负整数,本质上是一个索引值。顾名思义,这个索引值是用于标记打开的文件的。但是在网络编程中,文件描述符被用于标记创建的套接字对象。
通讯流程
得益于计算机网络的发展,套接字作为一种使用频率极高的协议目前已被编程语言很好地封装,这意味着我们可以十分轻易的使用套接字而不需要过分关注具体细节。我们主要使用以下函数进行套接字通讯的实现(此处初步了解即可,后面会详细介绍):
socket(); // 创建socket对象
bind(); // 绑定端口信息
listen(); // 监听端口
accept(); // 接受其他计算机连接
connect(); // 连接至其他计算机
sent(); // 发送数据
recv(); // 接收数据
write(); // 写入数据
read(); // 读取数据
注意:以上函数在实际使用中往往包含多个参数。但这里本文中我们只包含最重要的文件描述符作为参数,函数的具体用法其他文章介绍地很详细了我们这里就不再赘述
为了可以快速上手,我们先不按顺序来,先从最核心的发送/接收消息开始。
send/recv
发送接收消息可以使用 send/recv ,也可以使用 write/read 。本文中我们主要使用 send 跟 recv 。在上述函数中,我们都需要一个文件标识符来选择目标。当然,文件标识符只是一个非负整数,存储的信息极为有限。好在我们在上面列出的这些函数全部都已经经过封装,可以仅用过一个整数作为索引获取到内存中储存着的目标的其他重要信息(ip协议版本,目标ip,目标端口号等)的地址,因此简便起见我们可以看做文件标识符直接指向目标。
假设服务器已经有了指向客户端的文件标识符 client_fd ,客户端也有了指向服务器的文件标识符 server_fd 。由接收信息的一方先使用 recv() 函数,这里需要注意,套接字连接的两端是对等的,连接两端的计算机都可以 recv() 与 send() 。这里假设服务器要接收客户端的消息,于是调用 recv(client_fd) ,由于进程中可能存在多个套接字,因此需要 client_fd 指明从接收消息的目标,随后服务器被 recv() 阻塞。客户端在需要发送消息的时候使用 send(server_fd) ,这里同理需要 server_fd 指明要发送的目标。随后服务器收到客户端发来的消息,进程继续进行。
send/recv 示例
// Server
recv(client_fd, recvBuf, BUF_LENGTH,0);
printf("Info from client: %s\n",recvBuf);
// Info from client: Hello!
// Client
send(server_fd,"Hello",BUF_LENGTH,0);
套接字初始化
聊完了核心通讯流程,我们来讲讲套机接字的初始化。首先我们使用 socket() 函数创建套接字,使用 socket_fd = socket([args]) 即可获得一个指向目标的 socket_fd 。但注意,这只是一个空的套接字框架,并不包含实质信息。我们还需要 bind(socket_fd,[args]) 把端口号等初始化信息填入 socket_fd 指向的套机字里,或者说,绑定端口到套机字。
当然,除了 bind 本地端口与套接字,我们还需要像上面的例子一样获取对方计算机的套接字信息。这里使用 client_fd = accept(listen_fd) listen_fd 在下一小节介绍 即可获取到客户端绑定好端口发过来的 client_fd ,与 recv 一样, accept 也会阻塞进程。知道收到客户端发来的 client_fd 进程才会继续运行。当然客户端要想把 client_fd 发送过来就要 connect(server_fd) 把 bind 好了的 server_fd 发送给服务器。这里大家不用担心, connect 与 accept 会自动调整目标ip地址与源ip地址,客户端通过 connect 发送的指向服务器的 server_fd 在服务器通过 accept 收到后,会自动转成指向客户端的 client_fd 。server_fd 与 client_fd虽然指向的不是一个目标,但他们指向的是同一个套接字连接的两端。
listen_fd与监听
上一小节中我们已经用到了新文件标识符 listen_fd ,这是除 server_fd、client_fd 以外的第三个文件标识符。相信大家也发现了既然必须建立玩套接字连接才能使用 send/recv 来传送消息,那么客户端怎么把 client_fd 发给服务器呢?这里 listen_fd 便派上用场了。listen_fd 在创建完成绑定好初始化信息后,通过 listen(listen_fd) 函数监听客户端连接,这个函数不会阻塞进程。
实际执行顺序
现在我们按照实际执行顺序梳理一遍:
- 服务器 listen_fd = socket() 创建监听套接字。
- 服务器 bind(listen_fd) 给 listen_fd 指定需要监听的本机网卡(也可以监听本机所有所有网卡)。
- 客户端 server_fd = socket() 创建通讯套接字。
- 客户端 bind(server_fd) 初始化套接字,其中包含了目的IP地址与端口。
- 服务器 client_fd = accept(listen_fd) 准备 accept 。
- 客户端 connect(server_fd) ,向服务器建立连接,同时建立连接请求被 listen 监听到,建立监听套接字,随后通过 server_fd 包含的信息发送过去(具体实现细节对用户透明,我也不是很清楚就不乱讲了。这里“监听套接字”只用于描述这一过程,大家要是感兴趣具体什么技术实现的可以去源码找找答案)。accept 根据 listen_fd 监听到的客户端发来的 server_fd ,进行处理后反转成指向客户端的 client_fd 。
- 自此,客户端与服务器均获得了指向对方的文件描述符,可以开始用 send/recv 传递信息。
- 通讯结束时,可以由双方任意与一方使用 close(socket_fd) 结束连接。socket_fd 指的是通讯套接字连接中指向对方的文件描述符。
需要注意的是,监听套接字并不会关闭,通讯套接字关闭后监听套接字仍会保持监听,准备 accept 下一个通讯套接字连接。另外 close() 被调用后套接字也不会立即结束,还需要下层TCP协议向对方汇报连接结束等收尾工作。
附录:实现Demo
Demo 是根据B站视频写的,大家也可以去看看视频参考一下:[socket/网络编程]C语言实现ftp文件传输服务器——超简单,一学就会_哔哩哔哩_bilibili
服务器端
ftp_server.h
#pragma once
#include <winSock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <stdbool.h>
// 宏定义
#define PORT 2121 // 服务器端口号
#define MAX_BUFFER 1024
#define SERVER_IP "127.0.0.1"
// 初始化socket库
bool initSocket();
// 关闭socket库
bool closeSocket();
// 监听客户端连接
void listenToClient();
// 处理消息
bool processMsg(SOCKET);
ftp_server.c
#include <stdio.h>
#include "ftp_server.h"
int main()
{
printf("Server\n");
initSocket();
listenToClient();
closeSocket();
return 0;
}
// 初始化socket库
bool initSocket()
{
WSADATA wsadata;
if(0 != WSAStartup(MAKEWORD(2, 2), &wsadata))
{
printf("WSAStartup faild:%d\n", WSAGetLastError());
return false;
}
return true;
}
// 关闭socket库
bool closeSocket()
{
if (0 != WSACleanup())
{
printf("WSACleanup faild:%d\n", WSAGetLastError());
return false;
}
return true;
}
// 监听客户端连接
void listenToClient()
{
// 创建server套接字
SOCKET serfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == serfd)
{
printf("Socket failed:%d\n", WSAGetLastError());
return;
}
// 绑定IP跟端口号
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(PORT);
serAddr.sin_addr.S_un.S_addr = ADDR_ANY;
if (0 != bind(serfd, (struct sockaddr*)&serAddr, sizeof(serAddr)))
{
printf("Bind failed:%d\n", WSAGetLastError());
return;
}
// 监听客户端连接
if (0 != listen(serfd, 10))
{
printf("Listen failed:%d\n", WSAGetLastError());
return;
}
// 接收连接
struct sockaddr_in cliAddr;
int len = sizeof(cliAddr);
SOCKET clifd = accept(serfd, (struct sockaddr*)&cliAddr, &len);
if (INVALID_SOCKET == clifd)
{
printf("Accept failed:%d\n", WSAGetLastError());
return;
}
// 处理消息
while (processMsg(clifd))
{
}
}
// 处理消息
bool processMsg(SOCKET clifd)
{
printf("hello\n");
char recvBuf[1024] = {0};
recv(clifd, recvBuf, 1023, 0);
printf("%s\n", recvBuf);
return true;
}
客户端
ftp_client.h
#pragma once
#include <stdbool.h>
#pragma comment(lib,"ws2_32.lib")
#include <winsock2.h>
// 宏定义
#define PORT 2121 // 端口号
#define MAX_BUFFER 1024
#define SERVER_IP "10.1.4.142"
// 定义标记
enum MSGTAG
{
MSG_FILENAME = 1, // 文件名
MSG_FILESIZE = 2, // 文件大小
MSG_READY_READ = 3, // 准备接受
MSG_SEND = 4, // 发送
MSG_SUCCESSED = 5, // 传输完成
};
struct MsgHeader// 封装消息头
{
enum MSGTAG msgID; // 当前消息标记
struct
{
int fileSize; // 文件大小
char fileName[256]; // 文件名
}fileInfo;
};
// 初始化socket库
bool initSocket();
// 关闭socket库
bool closeSocket();
// 监听客户端
void connectToHost();
// 处理消息
bool processMsg(SOCKET);
ftp_client.c
#include <stdio.h>
#include "ftp_client.h"
int main()
{
printf("Client\n");
initSocket();
connectToHost();
closeSocket();
return 0;
}
// 初始化socket库
bool initSocket()
{
WSADATA wsadata;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsadata))
{
printf("WSAStartup faild:%d\n", WSAGetLastError());
return false;
}
return true;
}
// 关闭socket库
bool closeSocket()
{
if (0 != WSACleanup())
{
printf("WSACleanup faild:%d\n", WSAGetLastError());
return false;
}
return true;
}
// 监听客户端连接
void connectToHost()
{
// 创建server套接字
SOCKET serfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == serfd)
{
printf("Socket failed:%d\n", WSAGetLastError());
return;
}
// 绑定IP跟端口号
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(PORT);
serAddr.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);// 服务器IP地址
// 连接到服务器
if (0 != connect(serfd, (struct sockaddr*)&serAddr, sizeof(serAddr)))
{
printf("Connect failed:%d\n", WSAGetLastError());
return;
}
// 处理消息
while (processMsg(serfd))
{
Sleep(1000);
}
}
// 处理消息
bool processMsg(SOCKET serfd)
{
printf("Hello\n");
char fileName[1024] = "Hello from another side";
//gets_s(fileName, 1023);// 获取要下载的文件
struct MsgHeader file;
file.msgID = MSG_FILENAME;
strcpy(file.fileInfo.fileName,fileName);
send(serfd, fileName,strlen(fileName) + 1,0);
return true;
}