简介

Berkeley Socket套接字

套接字( Socket)最初是由加利福尼亚大学Berkeley分校为UNIX操作系统开发的网络通信接口,20世纪80年代初,加利福尼亚大学 Berkeley将美国国防部高研署提供的tC/iP集成到Unix中,并很快开发了TCP/IP应用程序接口(API),即 Socket(套接字)接口随着UNIX操作系统的广泛使用,套接字成为当前最流行的网络通信应用程序接口之一。

WinSocket套接字

90年代初,由 Sun Microsystems, JSB Corporation, FTP software, Microdyne和 MicrosoftW等几家公司共同制定了一套标准,即 Sockets规范。它是 Berkeley Sockets的重要扩充,主要体现在它增加了一些异步函数和符合 Windows消息驱动特性的网络事件异步选择机制。 Windows Sockets规范是一套开放的、支持多种协议的 Windows下的网络编程接口。目前实际应用中的 Windows Sockets规范主要有1.1版和2.0版,其中1.1版只支持TCP/IP协议,而2.0版支持多协议,并具有良好的向后兼容性。

创建流程

客户端

初始化套接字

1
2
3
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

建立socket

1
2
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(client != INVALID_SOCKET, "套接字创建失败!");
  • domain 通常为PF_INET,表示互联网协议(TCP/IP)
  • type 指定了Socket的类型 SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
  • protocol 通常赋值为0

绑定socket

1
2
3
4
5
6
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = connect(client, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字绑定失败!");
  • scokfd Socket()函数返回的Socket套接字

  • MyAddrr 指向含有本机IP地址及端口号的sockaddr类型的指针

    • sockaddr
    1
    2
    3
    4
    struct sockaddr{
    unsigned short as_family;//地址族,AF_xxx
    char sa_data[14]; //14字节的协议地址
    }
    1
    2
    * **sa_family** 一般为*AF_INET*,代表TCP/IP
    * **sa_data** 包含socket的IP地址和端口号
    • sockaddr_in 这个结构更加通用,与socketaddr结构体类似,且他们的指针可以互相替代
    1
    2
    3
    4
    5
    6
    struct sockaddr_in{
    short int sin_family;//地址族
    unsignedshort int sin_port;//端口号
    struct in_addr sin_addr;//IP地址
    unsigned char sin_zero[8];//填充0,以与 struct sockaddr大小保持一致
    }
    1
    2
    3
    4
    * **sin_family** 必须设置为*AF_INET*
    * **sin_port** 端口号
    * **sin_addr** 一个unsigned long的IP地址
    * **sin)zero** 填充0,用于与sockaddr大小保持一致

注意*sin_port*和*sin_addr*需要转换成网络字节优先顺序

客户端首发消息

1
2
3
4
// 对于 TCP 程序通过 send 和 recv 收发数据,函数是阻塞
char buffer[0x100] = { 0 };
recv(client, buffer, 0x100, 0);
printf("server: %s\n", buffer);

面向连接数据的发送

1
int send(int sockfd, const void *msg, int len, int flags);
  • sockfd 监听的套接字
  • msg 指向要发送的数据
  • len 以字节为单位的数据长度
  • flags 一般设置为0
  • 返回值为实际发送出去的字节数

面向连接的数据接收

1
int recv(int sockfd, void *buf, int len, int flags);
  • sockfd 监听的套接字
  • buf 存放接收数据的缓冲区
  • len 以字节为单位的数据长度
  • flags 一般设置为0
  • 返回值为实际接收到的数据

无连接的数据发送

int sendto(int sockfd, const void *msg, int len, int flags, const struct sockaddr \*to, int tolen);****
这个函数比*send()*函数多了两个参数

  • to 要发送数据到的目的主机的IP地址和端口号信息
  • tolen 通常别赋值为sizeof(struct sockaddr)
  • 返回值为实际发送出去的字节数

无连接的数据接收

int recvfrom(int sockfd, void *buf, int len, int flag, struct sockaddr *from, int \*fromlen);****
这个函数比*recv()*函数多了几个参数

  • from 是一个struct sockaddr类型的变量,保存数据来源主机的IP地址和端口号
  • fromlen 一般设置为sizeof(stuct sockaddr)
  • 返回值为实际接收到的数据

关闭套接字

1
2
closesocket(client);
WSACleanup();
  • 停止socket上面的全部操作
    closesocket(sockfd);
  • 关闭socket上面的某一个操作
    int shutdown(int sockfd, int how);
    how有几个可选的值
    • 0:不允许继续接收数据
    • 1:不允许接续发送数据
    • 2:不允许继续发送和接收数据

服务端

初始化

1
2
3
4
// 1. 初始化套接字模块,必须是网络程序中第一个调用的函数(搜索信号)
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

创建套接字

1
2
3
//	参数分别是使用的协议种类,数据传输方式以及协议类型
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(server != INVALID_SOCKET, "套接字创建失败!");

绑定

1
2
3
4
5
6
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = bind(server, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字绑定失败!");

监听

1
2
result = listen(server, SOMAXCONN);
check_result(result != SOCKET_ERROR, "套接字监听失败!");

服务器端程序调用listern()函数使得socket处于一个别动监听的模式,并且为这个socket建立一个输入数据队列,将到达服务器的请求保存到此队列中,直到程序处理。
int listen(int sockfd, int backlog);

  • sockfd 调用socket()函数返回的socket套接字
  • backlog 指定在请求队列中允许的最大请求数
  • 缓存队列中的请求,等待accept处理

等待客户端连接

1
2
3
4
int size = sizeof(SOCKADDR_IN);
SOCKADDR_IN client_addr = { 0 }; // 用于接收客户端信息(来电显示)
SOCKET client = accept(server, (SOCKADDR*)&client_addr, &size);
check_result(client != INVALID_SOCKET, "客户端接收失败!");

建立好缓存队列后,服务器端程序可以调用accept()函数处理客户的连接请求。
int accept(int sockfd, void *addr, int *addrlen);

  • sockfd 被监听的socket套接字
  • addr 通常是一个指向sockaddr_in变量的指针,该变量用于存储提出连接请求的主机信息
  • addrlen 通常是一个指向值为sizeof(struct sockaddr_in)的整型指针变量

关闭套接字

1
2
3
4
5
closesocket(client);
closesocket(server);

// 8. 清理套接字模块坏境
WSACleanup();

案例

案例1

聊天室服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 0. 添加必要的头文件,并且链接到相应的静态库
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <map>
#include <stdio.h>
#include <string>
#include <algorithm>
#include <ws2tcpip.h>
using namespace std;

// 用于保存当前连接到服务器的所有客户端
map<SOCKET, string> clients;

// 检查函数的执行结果是否正确
void check_result(bool result, const char* msg)
{
// 如果传入的表达式位假,意味着出错
if (result == false)
{
printf("error: %s\n", msg);
system("pause"); exit(0);
}
}

// 用于接收服务器消息的线程
DWORD CALLBACK RecvThread(LPVOID param)
{
// 由于参数是套接字对象,所以在这里接收
SOCKET client = (SOCKET)param;
char buffer[0x100] = { 0 };

// 循环的接收客户端发送过来的数据(当返回值<=0表示断开连接)
while (recv(client, buffer, 0x100, 0) > 0)
{
// 服务器在接收到消息之后,只负责转发消息
for (auto& sock : clients)
{
// 如果发送者和遍历到的是同一个,就不发
if (sock.first == client)
continue;

// 在每一条消息之前添加一个发送者的用户名
string msg = clients[client] + ": " + buffer;

send(sock.first, msg.c_str(), msg.length() + 1, 0);
}
}

// 如果结束了循环,就表示这个循环所属的套接字断开了连接,移除列表
// clients.erase(find(clients.begin(), clients.end(), client));
printf("%s 离开了服务器\n", clients[client].c_str());

return 0;
}

int main()
{
// 1. 初始化套接字模块,必须是网络程序中第一个调用的函数(搜索信号)
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

// 2. 创建一个套接字,应该保存[IP:PORT](买一部手机)
// 参数分别是使用的协议种类,数据传输方式以及协议类型
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(server != INVALID_SOCKET, "套接字创建失败!");

// 3. 将套接字绑定到指定的 ip 地址和端口(安装手机卡)
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = bind(server, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字绑定失败!");

// 4. 开启套接字的监听状态,第二个参数是同一时刻可以等待的客户端数量(开机)
result = listen(server, SOMAXCONN);
check_result(result != SOCKET_ERROR, "套接字监听失败!");

while (TRUE)
{
// 5. 等待客户端的连接,返回值是连接到的客户端(等接电话)
int size = sizeof(SOCKADDR_IN);
SOCKADDR_IN client_addr = { 0 }; // 用于接收客户端信息(来电显示)
SOCKET client = accept(server, (SOCKADDR*)&client_addr, &size);
check_result(client != INVALID_SOCKET, "客户端接收失败!");

// 如果有客户端连接到了服务器,要求给服务器传递一个昵称
char nickname[0x100] = { 0 };
recv(client, nickname, 0x100, 0);

// [如果有客户端连接,就放入到客户端容器中]
printf("%s 连接到了服务器\n", nickname);
clients[client] = nickname;

// 6. 为每一个客户端创建一个线程,用于执行信息的接收操作
CreateThread(NULL, NULL, RecvThread, (LPVOID)client, NULL, NULL);
}

// 8. 当数据处理结束之后,需要断开连接
closesocket(server);

// 9. 清理套接字模块坏境
WSACleanup();

system("pause");
return 0;
}

聊天室客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 0. 添加必要的头文件,并且链接到相应的静态库
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <vector>
#include <stdio.h>
#include <ws2tcpip.h>

// 检查函数的执行结果是否正确
void check_result(bool result, const char* msg)
{
// 如果传入的表达式位假,意味着出错
if (result == false)
{
printf("error: %s\n", msg);
system("pause"); exit(0);
}
}

// 用于接收服务器消息的线程
DWORD CALLBACK RecvThread(LPVOID param)
{
// 由于参数是套接字对象,所以在这里接收
SOCKET client = (SOCKET)param;
char buffer[0x100] = { 0 };

// 循环的接收服务器发送过来的数据(当返回值<=0表示断开连接)
while (recv(client, buffer, 0x100, 0) > 0)
printf("%s\n", buffer);

return 0;
}

int main()
{
// 1. 初始化套接字模块,必须是网络程序中第一个调用的函数(搜索信号)
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

// 2. 创建一个套接字,应该保存[IP:PORT](买一部手机)
// 参数分别是使用的协议种类,数据传输方式以及协议类型
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(client != INVALID_SOCKET, "套接字创建失败!");

// 3. 将套接字绑定到指定的 ip 地址和端口(安装手机卡)
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = connect(client, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字连接失败!");

// 给服务器传递一个用户名
char buffer[0x100] = { 0 };
scanf_s("%s", buffer, 0x100);
send(client, buffer, 0x100, 0);

// 4. 创建一个线程,用于执行信息的接收操作
CreateThread(NULL, NULL, RecvThread, (LPVOID)client, NULL, NULL);

// 5. 使用 send 函数向服务器发送消息
while (scanf_s("%s", buffer, 0x100) == 1
&& strcmp(buffer, "quit"))
send(client, buffer, strlen(buffer) + 1, 0);

// 6. 当数据处理结束之后,需要断开连接
closesocket(client);

// 7. 清理套接字模块坏境
WSACleanup();

system("pause");
return 0;
}

案例2

文件发送端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// 0. 添加必要的头文件,并且链接到相应的静态库
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <stdio.h>
#include <string.h>
#include <ws2tcpip.h>

// 块的大小,
const int block_size = 1024*1024;


// 描述文件信息的结构体
typedef struct _MY_FILE_INFO
{
char name[0x100]; // 文件名称
int block_count; // 块的数量
} MY_FILE_INFO, * PMY_FILE_INFO;

// 描述块信息的结构体
typedef struct _BLOCK_INFO
{
int index; // 当前是文件中的第几部分
char data[block_size]; // 每一个块的内容
DWORD size; // 当前实际发送的长度
} BLOCK_INFO;

// 检查函数的执行结果是否正确
void check_result(bool result, const char* msg)
{
// 如果传入的表达式位假,意味着出错
if (result == false)
{
printf("error: %s\n", msg);
system("pause"); exit(0);
}
}

// 先发送文件的信息到接收方
HANDLE send_file_info(SOCKET sock, LPCSTR filename, PMY_FILE_INFO info)
{
// 如果目标文件存在,就以只读方式打开并允许别人访问
HANDLE file = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

// 获取到文件的大小,用于计算一共要发送的次数
DWORD size = GetFileSize(file, NULL);

// 根据大小计算一共有多少个块,
int count = size % block_size == 0 ? size / block_size : size / block_size + 1;

// 组合成一个结构体传递给接收方
info->block_count = count;
memcpy(info->name, filename, strlen(filename) + 1);

// 发送给接收方
send(sock, (char*)info, sizeof(*info), 0);

return file;
}

// 发送文件的内容
VOID send_file_data(SOCKET sock, HANDLE file, PMY_FILE_INFO info)
{
for (int i = 0; i < info->block_count; ++i)
{
// 0. 为每一个块创建结构体进行描述
BLOCK_INFO* block_info = new BLOCK_INFO{ i };

// 1. 读取每一个固定大小的块,并发送
ReadFile(file, block_info->data, block_size, &block_info->size, NULL);
send(sock, (char*)block_info, sizeof(BLOCK_INFO), 0);

Sleep(100);
}

CloseHandle(file);
}

int main()
{
// 1. 初始化套接字模块,必须是网络程序中第一个调用的函数(搜索信号)
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

// 2. 创建一个套接字,应该保存[IP:PORT](买一部手机)
// 参数分别是使用的协议种类,数据传输方式以及协议类型
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(server != INVALID_SOCKET, "套接字创建失败!");

// 3. 将套接字绑定到指定的 ip 地址和端口(安装手机卡)
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = bind(server, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字绑定失败!");

// 4. 开启套接字的监听状态,第二个参数是同一时刻可以等待的客户端数量(开机)
result = listen(server, SOMAXCONN);
check_result(result != SOCKET_ERROR, "套接字监听失败!");

// 5. 等待客户端的连接,返回值是连接到的客户端(等接电话)
int size = sizeof(SOCKADDR_IN);
SOCKADDR_IN client_addr = { 0 }; // 用于接收客户端信息(来电显示)
SOCKET client = accept(server, (SOCKADDR*)&client_addr, &size);
check_result(client != INVALID_SOCKET, "客户端接收失败!");


MY_FILE_INFO info = { 0 };
HANDLE file = send_file_info(client, "demo.exe", &info);
send_file_data(client, file, &info);

// 7. 当数据处理结束之后,需要断开连接
closesocket(client);
closesocket(server);

// 8. 清理套接字模块坏境
WSACleanup();

system("pause");
return 0;
}

文件接收端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 0. 添加必要的头文件,并且链接到相应的静态库
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <stdio.h>
#include <ws2tcpip.h>

// 块的大小
const int block_size = 1024 * 1024;

// 描述文件信息的结构体
typedef struct _MY_FILE_INFO
{
char name[0x100]; // 文件名称
int block_count; // 块的数量
} MY_FILE_INFO, *PMY_FILE_INFO;

// 描述块信息的结构体
typedef struct _BLOCK_INFO
{
int index; // 当前是文件中的第几部分
char data[block_size]; // 每一个块的内容
int size; // 当前实际发送的长度
} BLOCK_INFO;

// 检查函数的执行结果是否正确
void check_result(bool result, const char* msg)
{
// 如果传入的表达式位假,意味着出错
if (result == false)
{
printf("error: %s\n", msg);
system("pause"); exit(0);
}
}

// 先发送文件的信息到接收方
void recv_file_info(SOCKET sock, PMY_FILE_INFO info)
{
// 接收结构体信息
recv(sock, (char*)info, sizeof(*info), 0);
printf("%s %d\n", info->name, info->block_count);
}

// 先发送文件的信息到接收方
void recv_file_data(SOCKET sock, PMY_FILE_INFO info)
{
// 使用传递过来的名称创建一个新的文件
HANDLE file = CreateFileA(info->name, GENERIC_WRITE, NULL,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// 通过一个循环向其中写入数据
for (int i = 0; i < info->block_count; ++i)
{
// 定义一个块信息的结构体,接收发送到的数据
BLOCK_INFO* block_info = new BLOCK_INFO{ 0 };
recv(sock, (char*)block_info, sizeof(BLOCK_INFO), 0);

// 设置文件指针的位置到相应的起始点
SetFilePointer(file, block_size * block_info->index, 0, FILE_BEGIN);

// 向文件内写入内容
DWORD bytes = 0;
WriteFile(file, block_info->data, block_info->size, &bytes, NULL);

printf("%d 写入成功,大小为 %d\n", block_info->index, bytes);
}

CloseHandle(file);
}

int main()
{
// 1. 初始化套接字模块,必须是网络程序中第一个调用的函数(搜索信号)
WSADATA wsadata = { 0 };
int result = WSAStartup(MAKEWORD(2, 2), &wsadata);
check_result(!result && wsadata.wVersion == 0x0202, "套接字环境初始化失败!");

// 2. 创建一个套接字,应该保存[IP:PORT](买一部手机)
// 参数分别是使用的协议种类,数据传输方式以及协议类型
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(client != INVALID_SOCKET, "套接字创建失败!");

// 3. 将套接字绑定到指定的 ip 地址和端口(安装手机卡)
SOCKADDR_IN server_addr = { 0 };
server_addr.sin_family = AF_INET; // 协议
server_addr.sin_port = htons(0x1515); // 端口
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
result = connect(client, (SOCKADDR*)&server_addr, sizeof(server_addr));
check_result(result != SOCKET_ERROR, "套接字绑定失败!");

// 4. 对于 TCP 程序通过 send 和 recv 收发数据,函数是阻塞


MY_FILE_INFO info = { 0 };
recv_file_info(client, &info);
recv_file_data(client, &info);


// 5. 当数据处理结束之后,需要断开连接
closesocket(client);

// 6. 清理套接字模块坏境
WSACleanup();

system("pause");
return 0;
}