0. 套接字
网上很多说套接字名称包括:
IP:port
- 在 《Windows Socket 网络编程》 Bob Quinn 著的一书中,套接字名称包括:
1. IP
2. port
3. protocol (协议)
- 服务端创建套接字
(IP, port, protocol)
和客户端的套接字(IP, port, protocol)
可以通过套接字编程连接起来。Windows
有WinSOCKET
编程,Linux
有linux socket
编程,两者不兼容。
环境:
VS 2019
1. 图示流程(图网上找的,代码自己改的)
- 说明:刚学习 windows socket 编程,能搞懂图中的函数怎么用及其参数的意义算是入门了。
2. 库 <winsock2.h>
#include <iostream>
#include <winsock2.h>
#include <string>
// 使用 <winsock2.h>,需要链接的动态链接库 "Ws2_32.lib"
#pragma comment(lib, "Ws2_32.lib")
- 注: 不使用旧版本的
<winsock.h>
,使用较新的<winsock2.h>
3. 初始化动态链接库
// 初始化 DLL
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsdata;
if (WSAStartup(sockVersion, &wsdata) != 0) {
return EXIT_FAILURE;
}
解释
- :
WORD
: 一个宏定义,一个无符号短整型
typedef unsigned short WORD;
MAKEWORD
: 一个宏定义,获取Windows Sockets 规范的版本
,(2, 2)
说明获取2.2
版本,到2021-01-10
最高为2.2
版本
#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
WSADATA
: 一个结构体,存放windows socket
初始化信息,具体如下(看注释):
typedef struct WSAData {
WORD wVersion; // 将使用的 Winsock 版本号
WORD wHighVersion; // 载入的 Winsock 动态库支持的最高版本,高字节代表次版本,低字节代表主版本
#ifdef _WIN64 // 如果是 64 位
unsigned short iMaxSockets; // 最大数量的并发 Sockets,其值依赖于可使用的硬件资源
unsigned short iMaxUdpDg; // 数据报的最大长度
char FAR * lpVendorInfo; // 为 Winsock 实现而保留的制造商信息
char szDescription[WSADESCRIPTION_LEN+1]; // 由特定版本的 Winsock 设置,实际上没有太大用处。
char szSystemStatus[WSASYS_STATUS_LEN+1]; // 同上
#else
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
#endif
} WSADATA, FAR * LPWSADATA;
注释参考: https://blog.csdn.net/huachizi/article/details/89401476
WSAStartup(sockVersion, &wsdata)
: 重点,通过进程启动Winsock DLL
的使用。
int WSAAPI WSAStartup(
_In_ WORD wVersionRequested,
_Out_ LPWSADATA lpWSAData
);
如果成功,WSAStartup
函数将返回 0
。否则,它将返回下面列出的错误代码之一。
4. 正文:进入 1. 图示流程
中流程图的操作
4.1 服务端,按流程图实现
- 代码
#include <iostream>
#include <winsock2.h>
#include <string>
#pragma comment(lib, "Ws2_32.lib")
using namespace std;
int main() {
// 初始化 DLL
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsdata;
if (WSAStartup(sockVersion, &wsdata) != 0) {
return EXIT_FAILURE;
}
// 创建套接字
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serverSocket == INVALID_SOCKET) {
cout << "create socket failure!!" << endl;
return EXIT_FAILURE;
}
// 绑定套接字
sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
socketAddr.sin_port = htons(8080);
socketAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(serverSocket, (sockaddr*)&socketAddr, sizeof(socketAddr)) == SOCKET_ERROR) {
cout << "bind error!" << endl;
return EXIT_FAILURE;
}
// 监听
if (listen(serverSocket, 10) == SOCKET_ERROR) {
cout << "listen error!!" << endl;
}
// 接收客户端套接字
SOCKET clientSocket;
sockaddr_in client_sin;
// store message
int len = sizeof(client_sin);
cout << "waitting to connect......" << endl;
clientSocket = accept(serverSocket, (sockaddr*)&client_sin, &len);
if (clientSocket == INVALID_SOCKET) {
cout << "accept error" << endl;
return EXIT_FAILURE;
}
else {
cout << "get a connection: " << inet_ntoa(client_sin.sin_addr) << endl;
}
// 看流程图,循环
while (true) {
// 接收消息
const size_t messageLen = 1000;
char recClientMsg[messageLen];
int num = recv(clientSocket, recClientMsg, messageLen, 0);
// 截断数组
recClientMsg[num] = '\0';
if (num > 0) {
cout << "Client say: " << recClientMsg << endl;
}
// 退出
if (strcmp(recClientMsg, "exit()") == 0) {
break;
}
// 发送数据
cout << "Input message you want to send to client:" << endl;
string data;
getline(cin, data);
const char* sendClientMsg;
sendClientMsg = data.c_str();
send(clientSocket, sendClientMsg, data.size(), 0);
// 退出
if (strcmp(sendClientMsg, "exit()") == 0) {
break;
}
}
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return EXIT_SUCCESS;
}
4.2 客户端,按流程图实现
- 代码
#include <winsock2.h>
#include <iostream>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
int main() {
// 初始化
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0) {
return EXIT_SUCCESS;
}
// 创建客户端套接字
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET) {
cout << "Socket error" << endl;
return EXIT_FAILURE;
}
// 连接服务端
sockaddr_in sock_in;
sock_in.sin_family = AF_INET;
sock_in.sin_port = htons(8080);
sock_in.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(clientSocket, (sockaddr*)&sock_in, sizeof(sock_in)) == SOCKET_ERROR) {
cout << "Connect error" << endl;
return EXIT_FAILURE;
}
else {
cout << "Connect successfully." << endl;
}
// 看流程图,循环
while (true) {
// 发送信息
cout << "Input message you want send:" << endl;
string data;
getline(cin, data);
const char* sendServerMsg;
sendServerMsg = data.c_str();
send(clientSocket, sendServerMsg, strlen(sendServerMsg), 0);
// 判断结束循环
// 退出
if (data == "exit()") {
break;
}
// 接收信息
const size_t messageLen = 1000;
char revServerMsg[messageLen];
int num = recv(clientSocket, revServerMsg, messageLen, 0);
// 截断数组
revServerMsg[num] = '\0';
if (num > 0) {
cout << "Sever say:" << revServerMsg << endl;
}
// 判断结束循环
// 退出
if (strcmp(revServerMsg, "exit()") == 0) {
break;
}
}
// 关闭客户端套接字
closesocket(clientSocket);
WSACleanup();
return EXIT_SUCCESS;
}
4.3 启动
注意记得修改按 Alt
+ P
+ P
(Alt
按住, P
按两下):
1. 启动服务端
- 服务端:
2. 再启动客户端
- 客户端:
- 服务端:
4.4 问题
- 简单实现果然还有还多问题,勉强能进行
一问一答
式通信
5. Linux socket
- server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 4096
#define PORT 6666
int main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
int n;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("======waiting for client's request======\n");
while (1)
{
if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) == -1)
{
printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
continue;
}
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connfd);
}
close(listenfd);
}
- client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 4096
#define PORT 6666
int main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE], sendline[MAXLINE];
struct sockaddr_in servaddr;
if (argc != 2)
{
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("inet_pton error for %s\n", argv[1]);
exit(0);
}
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, MAXLINE, stdin);
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
close(sockfd);
exit(0);
}
5.1 服务端:
g++ -o server server.c
# 生成 server 执行文件
./server
- 查看端口
6666
占用
[chen@hecs-70768 socket]$ lsof -i:6666
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 11795 chen 3u IPv4 4725561 0t0 TCP *:ircu-2 (LISTEN)
5.2 客户端
g++ -o client client.c
# 生成 client 执行文件
# xxx.xxx.xxx.xxx 是服务端的 IP,后面会提示你输入文字,在这里先不输入:
zhiyong@LAPTOP-OC4RD91F:/mnt/d/Computer/network/socket$ ./client xxx.xxx.xxx.xxx
send msg to server:
5.3 在服务端的机器上面查看端口占用:
会显示服务端的端口显示ESTABLISHED
[chen@hecs-70768 socket]$ lsof -i:6666
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 11795 chen 3u IPv4 4725561 0t0 TCP *:ircu-2 (LISTEN)
server 11795 chen 4u IPv4 4727103 0t0 TCP hecs-70768:ircu-2->117.61.106.77:36486 (ESTABLISHED)
5.4 抓包
客户端执行 ./client xxx.xxx.xxx.xxx
的时候,首先执行 connect
,在内核中就是执行 TCP 三次握手。
客户端发起 connect
,服务端一直处于 listen
状态,然后在 accept
,在接收到客户端的 connect
后,返回一个客户端的套接字。然后这个客户端发送的数据,就是通过这个套接字去读取。读取的方法跟打开文件的 IO 一样。
5.5 客户端在 close 后,会进入 TIME_WAIT
模式
- 可以先在服务端使用
lsof -i:6666
查看客户端的端口,也可以通过抓三次握手的报文来获取端口。客户端发起连接的端口为59438
,使用命令netstat -a | grep 59438
查看,如下:
tcp 0 0 hecs-70768:ircu-2 hecs-70768:59438 TIME_WAIT
5.6 一台服务器能监听多少个客户端?
服务端中的 accept
函数返回一个客户端套接字 clientFd
,这个跟客户端的IP、端口相关。
理论上世界上所有的 IP 都可以作为客户端,每个客户端有 65536 个端口,而全世界的 IP 约有 四十亿个地址,所以有的文章说一台服务器最多有 四十亿个✖65546
。但是一台服务器需要一个监听套接字 serverFd
,当有一个客户端连接进来的时候,需要使用 accept
接收并建立一个套接字 clientFd
,而 accept
返回值是 int
类型,一般都大于 0
。而除去标准输入输出的 0, 1, 2
和自身的监听套接字serverFd
外,所以理论上最多应该是建立 int
最大值 - 4 的连接数。同时, 因为每条连接占内存,实际上一般就一百多万条就搞不住了。
5.7 TCP 四次握手
- 无论是服务端还是客户端,都可以主动的发起
close
操作,只要有一方发起,就会进行四次握手,这里说的发起连接是指:客户端就不用说了,就是指自身的套接字。服务端指的是accept
得到的客户端套接字clientFd
,而不是服务端的套接字serverFd
。如果关闭了服务端的套接字serverFd
,相当于服务端挂掉了。
下面是一个例子:
- 服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 4096
#define PORT 6666
int main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
int n;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("======waiting for client's request======\n");
// while (1)
{
if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) == -1)
{
printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
// continue;
}
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connfd); // 服务端先发起 close
}
close(listenfd); // 马上跟着就关闭了服务端的套接字,相当于服务端挂机了
}
- 客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 4096
#define PORT 6666
int main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE], sendline[MAXLINE];
struct sockaddr_in servaddr;
if (argc != 2)
{
printf("usage: ./client <ipaddress>\n");
exit(0);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("inet_pton error for %s\n", argv[1]);
exit(0);
}
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
printf("send msg to server: \n");
fgets(sendline, MAXLINE, stdin);
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
sleep(30); // 在这里等待 30s
close(sockfd);
exit(0);
}
结果(服务端:ircu-2
,客户端:594381
):
[chen@hecs-70768 ~]$ netstat -a | grep 70768
tcp 0 0 hecs-70768:ssh 117.61.106.77:36482 ESTABLISHED
tcp 1 0 hecs-70768:59438 hecs-70768:ircu-2 CLOSE_WAIT
tcp 0 0 hecs-70768:ircu-2 hecs-70768:59438 FIN_WAIT2
tcp 0 36 hecs-70768:ssh 117.61.106.77:36526 ESTABLISHED
服务端发起关闭,发包 FIN
,客户端回复 ACK
,接着客户端准备发送 FIN
在这个过程,马上关闭了 服务端套接字,服务端没有回复 ACK
,那么服务端就处于FIN_WAIT2
(因为是服务端主动发起连接的)。而客户端处于CLOSE_WAIT
状态。由于服务端没有回复 ACK
,客户端就重发它的 FIN
,一共重发 5 词(这个大概看系统)。还是没有回复(因为服务端挂了),此时客户端发起一个 RST
,将连接断开。
之后服务端的连接就关闭了,就进入到TIME_WAIT
状态。
tcp 0 0 hecs-70768:ircu-2 hecs-70768:59438 TIME_WAIT
所以,谁发起 close
连接,谁就有FIN_WAIT2
、TIME_WAIT
的状态,而对端就有 CLOSE_WAIT
状态