Socket
Socket 初识
Necessary foundation
IP地址
在全球互联网范围内唯一标识主机的地址
IPV4
32bit,4byte的地址,使用点分十进制法表示
可以使用的IP地址为2^32个
IPV6
128bit,16byte的地址,使用:分16进制法表示
可以使用的地址为2^128个
Port 端口
唯一标识主机中的某进程
16bit的 unsigned short (无符号短整型)数
可以使用的端口数目为2^16,有效端口范围为0~65535(2^16-1)
存储模式
大于一字节的数据在内存中的存储方式
大端存储
低内存地址 -> 高内存地址 存放 高字节数据 -> 低字节数据
小端存储
低内存地址 -> 高内存地址 存放 低字节数据 -> 高字节数据
如何判断主机的存储模式
基本思路
将一4byte无符号整形数据截取首字节的内容判断该首字节存放是高字节数据还是低字节数据
注意:
整数数据从左往右依次为 高字节 -> 低字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using namespace std;
int main() {
unsigned int a = 1;
unsigned int*p = &a;
//通过类型转型截取数据首字节的部分
unsigned char*p1 = (unsigned char*)p;
switch(*p1) {
case 1 :
cout<<"小端"<<endl;break;
case 0 :
cout<<"大端"<<endl;break;
default :
cout<<"无法判断"<<endl;break;
}
return 0;
}
字节序
大于一个字节类型的数据在内存中的存放顺序
char类型没有字节序问题,因为基本单位大小为1byte
Little-Endian
低内存地址 -> 高内存地址 存放 高字节数据 -> 低字节数据
Big-Endian
低内存地址 -> 高内存地址 存放 低字节数据 -> 高字节数据
主机字节序
多字节整数在内存中的存放顺序
不同CPU,操作系统有不同的字节序类型
网络字节序
TCP/IP协议规定好的数据的表示格式
它与具体的操作系统,CPU类型等无关,保证了数据在不同主机之间传输时的正确性
采用大端存储
跨平台通信
发送方与接收方都是从低内存地址开始发送/接受数据
而采用Big-Endian的接收方会按高字节->低字节的方式去存取传来的数据(认为传来的首字节为数据的高字节部分)
若通信双方采用不同字节序,则会导致发送方发送的数据被接收方错误解析,导致传送的数据出错!
字节序的转换
IP , Port端口等在通信时需要进行大小端转换
Port端口的转换
1
2
3
4
5
6
7
8
9
10
11
12
// htonl -> host to network long 将 32位长整数 由主机字节序 -> 网络字节序
//32bit长整型在不同字节序之间的转换
//不适用于IP地址,因为IP地址是字符串类型
unit32_t htonl(uint32_t hostlong);
unit32_t ntohl(unit32_t netlong);
//16bit短整型在不同字节序之间的转换
//适用于端口的转换
unit16_t htons(unit16_t hostshort);
unit16_t ntohs(unit16_t netshort);
IP地址的转换
32bit长整型在不同字节序之间的转换接口函数并不适用于IP地址
因为IP地址虽然本质是一个整形数,但是使用过程中都是字符串类型
字符串类型的主机字节序 <-> 整型类型的网络字节序
IPV4 - 仅适用于IPV4
1
2
3
4
5
6 //windows 与 linux 均含该接口函数
//字符串类型的主机字节序 -> 整型类型的网络字节序
unsigned int inet_addr(const char *p);
//整型类型的网络字节序 -> 字符串类型的主机字节序
char* inet_ntoa(struct in_addr in);sockaddr结构 –vs2017源码
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 //sockaddr结构 --vs2017源码
//sockaddr
struct sockaddr{
sa_family_t sa_family; //地址族协议 只能指定为IPV4
char sa_data[14]; //端口(2byte) + IP(4byte) + 填充(8byte)
};
//sockaddr_in
typedef struct sockaddr_in {
short sin_family;
ADDRESS_FAMILY sin_family;
USHORT sin_port; //大端字节序的 Port端口
//-----------------------------------------------
IN_ADDR sin_addr; //IN_ADDR 含大端字节序的 IP地址
//-----------------------------------------------
CHAR sin_zero[8]; //8字节填充
} SOCKADDR_IN, *PSOCKADDR_IN;
//IN_ADDR
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
//-----------------------------------------------
ULONG S_addr; //S_addr 大端字节序的 IP地址
//-----------------------------------------------
} S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;Linux IPV4-IPV6通用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 //仅Linux下含该接口函数,windows下无
//字符串类型的主机字节序 -> 整型类型的网络字节序
int inet_pton(int af,const char* src,void* dst);
//af: 协议族(IPV4-AF_INET IPV6-AF_INET6) 指定src的地址类型 点分10进制 or 冒号分16进制
//src: 待转换的字符串类型的IP地址
//dst: 转换成功后的整型大端字节序的IP地址并不以返回值返回,而是通过参数dst返回
//return:
//1: 成功转换 0: 无效的src IP地址 -1: 协议族类型与src IP地址不匹配
//整型类型的网络字节序 -> 字符串类型的主机字节序
const char* inet_ntop(int af,const void*src,char* dst,socklen_t size);
//af: 协议族(IPV4-AF_INET IPV6-AF_INET6) 指定src的地址类型 点分10进制 or 冒号分16进制
//src: 指向 待转换的整型类型的IP地址 内存的指针
//dst: 转换成功后的整型大端字节序的IP地址不仅以返回值返回,而是通过参数dst返回
//size: 标记 dst指向的内存 可存储的最大字节数
//return: 转换成功后的IP字符串 or 失败NULL
Socket - 本地局域网
应用层与TCP/IP协议族之间的抽象层
Facade门面模式的思想,用户只需操作该Socket接口来完成网络之间进程的通信而无需了解协议族细节
四元组(ClientIP , ClientPort , ServerIP , ServerPort)
Defination
一个套接字接口构成连接的一端,一对套接字接口确定一个连接
Socket 类型
文件描述符
一个文件描述符对应两块内存 读缓冲区 与 写缓冲区
文件描述符的作用
从读缓冲区中读数据,将数据写入到写缓冲区中
用于监听的 socket对象 / 文件描述符
只有一个
只负责监听客户端进程连接请求,不负责双方通信
当client 发送连接请求时,内核会将该请求存放于监听socket对象的读缓冲区中
在调用accept()时,再由内核检测该读缓冲区中是否有数据,若检测到该读缓冲区有数据不阻塞,建立连接,否则阻塞
用于通信的 socket对象 / 文件描述符
可以有多个,通信双方都有各自的通信socket描述符
负责相应的客户端进程与该服务端进程的通信,若有N个客户端进程与该服务端进程通信,则有N个相对应的socket描述符
进行数据通信时,发送方的先将通信数据写入到 通信socket对象 的写缓冲区中
再由内核检测对应写缓冲区中是否有数据,有则发送到网络中
接收方内核将接受到的数据存到相应的读缓冲区,再通过IO操作读取数据
基于TCP的套接字通信流程
Server
创建用于监听的socketA对象,该对象为一个文件描述符
1
int socketA = new socket(...);
将该socketA对象与server进程的 IP+Port 进行绑定
1
bind(...);
监听该socketA的连接情况
1
listen(...);
若有 client端进程 发起连接请求,进行回应并返回一个用于通信的socketB对象
1
2//TCP三次握手,开辟用于通信的资源
int socketB = accept(...);对该socketB对象进行IO操作,完成与client端进程的通信
1
2
3
4//接受数据
read(); / recv();
//发送数据
write(); / send();若client进程断开连接,则server也将用于通信的socketB对象关闭
1
2
3//socketB
//四次挥手的后两次挥手
close();若服务器不再启用,则关闭用于监听的socketA对象
1
2//socketA
close();
Client
创建用于通信的socket对象
1
int socket = new socket(...);
向指定 IP+Port 的server端进程 发起连接请求
1
2//三次握手建立连接,开辟通信资源
connect();对该socket对象进行IO操作,完成与server端进程的通信
1
2
3
4//接受数据
read(); / recv();
//发送数据
write(); / send();关闭该用于通信的socket对象
1
2//四次挥手的前两次挥手
close();
Interface - Linux
int socket(int domain,int type,int protocol);
创建socket对象,并返回唯一标识该socket对象的socket描述符
domain: 协议族(family),决定了socket的地址类型
AF_INET ipv4(32bit) + 端口号(16bit) 的组合
type: 传输层协议类型
SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
protocol: 协议
IPPROTO_TCP…
若为0,则自动选择type类型的协议
注意:
type 与 protocol 需要对应,一般直接将protocol置为0rerurn: a file descirptor for the new socket / 成功则返回该socket文件描述符 / 失败则返回-1
int bind(int server_sockfd,const struct sockaddr* server_addr,socklen_t addrlen);
将 socket对象 与指定 ( ip+port ) 建立绑定
server_sockfd: 将被绑定的 server端 socket对象
server_addr: 与 server_sockfd 进行绑定的协议地址( port+IP)
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//sockaddr结构 --vs2017源码
//sockaddr
struct sockaddr{
sa_family_t sa_family; //地址族协议 只能指定为IPV4
char sa_data[14]; //端口(2byte) + IP(4byte) + 填充(8byte)
};
//sockaddr_in
typedef struct sockaddr_in {
short sin_family;
ADDRESS_FAMILY sin_family;
USHORT sin_port; //大端字节序的 Port端口
//-----------------------------------------------
IN_ADDR sin_addr; //IN_ADDR 含大端字节序的 IP地址
//-----------------------------------------------
CHAR sin_zero[8]; //8字节填充
} SOCKADDR_IN, *PSOCKADDR_IN;
//IN_ADDR
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
//-----------------------------------------------
ULONG S_addr; //S_addr 大端字节序的 IP地址
//-----------------------------------------------
} S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;addrlen: 协议地址的长度,大小
return: 成功 0 / 失败 -1
注意:
server端需要在listen之前bind绑定固定地址,client无需手动绑定,而是在connect时由系统生成随机端口 + 自身IP的组合
client 与 server 的通信需要借助该addr来建立连接
在将地址绑定到 socket对象 的时候,注意一定要进行主机字节序 -> 网络字节序的转换 Port 与 IP 均需要进行大端转换
即使均采用big-endian,也需要显示写出转换函数,此时该函数不会进行转换,而是原样返回传入的参数
int listen(int server_sockfd,int backlog);
对绑定地址后的socket进行监听,等待客户的connect连接请求
server_sockfd: 被监听的 server端 socket对象
backlog: 该socket对象一次可检测到的最大客户端请求数,内核默认最大为128
return: 成功 0 / 失败 -1
int connect(int client_sockid,const struct sockaddr* addr,socklen_t addrlen);
client 向 server 发送连接请求
client_sockid: 发送请求的 client_socket对象
addr: 请求的目的地址,应该与被请求的 server端 server_addr相同
addrlen: 协议地址的长度
return: 成功 0 / 失败 -1
int accept(int server_sockfd, struct sockaddr* addr,socklen_t *addrlen);
server 接受一个客户端请求,与一个客户端建立连接
与多个客户端建立连接需要调用多次
server_sockfd: server端 socket对象
addr: 协议地址,传出参数,用于根据需求选择传出 client 协议地址(IP+Port)信息
addrlen: 指针 指向存放addr内存大小的那块内存的地址,既是传入参数(需要初始化)又是传出参数(可通过该参数返回值)
若addr为空,则addrlen也相应置空,若不为空,则将 存放addr内存大小的内存地址 传给addrlen指针
return: 成功 用于通信的socket描述符 / 失败 -1
read(),write()
IO接口
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//发送数据
ssize_t write(int sockfd, const void *buf, size_t count); //--A
/*
sockfd: 用于通信的socket描述符
buf: 传入要发送的数据
size: buf的最大容量,防止内存溢出
return: >0 实际发送的字节数 = len
-1 发送失败
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags); //--B
/*
sockfd: 用于通信的socket描述符
buf: 传入要发送的数据
size: buf的最大容量,防止内存溢出
flags: 特殊属性,一般设置为0
return: >0 实际发送的字节数 = len
-1 发送失败
*/
//接受数据
ssize_t read(int sockfd, void *buf, size_t size); //--A
/*
sockfd: 用于通信的socket描述符
buf: 暂存接受到的数据
size: buf的最大容量,防止内存溢出
return: >0 实际接受的字节数
=0 对方断开连接,不再阻塞直接返回0
-1 接受数据失败,阻塞等待数据到达,重新接受
*/
ssize_t recv(int sockfd, void *buf, size_t size, int flags); //--B
/*
sockfd: 用于通信的socket描述符
buf: 暂存接受到的数据
size: buf的最大容量,防止内存溢出
flags: 特殊属性,一般设置为0
return: >0 实际接受的字节数
=0 对方断开连接,不再阻塞直接返回0
-1 接受数据失败,阻塞等待数据到达,重新接受
*/close(int sockfd);
关闭socket对象
return: 成功 0 / 失败 -1
注意:
- close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
Soncket in Windows
初始化套接字环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//--------------加载套接字库---------------
//包含对应头文件
//加载相应的动态链接库 ws2_32.lib
//--------------初始化套接字环境---------------
WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
/*
wVersionRequested: 指定Windows Socket版本,一般使用的版本为2.2-MAKEWORD(2,2)
lpWSAData: WSAData 结构的指针
*/
//-------------注销Winsock相关库-----------
WSACleanup();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WSADATA wsaData;
WORD libversion = MAKEWORD(2, 2);
if (WSAStartup(libversion, &wsaData) != 0) {
cout << "Init Operaing error." << endl;
return 0;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
WSACleanup();
cout << "Can'find valid lib." << endl;
}
其余基本与Linux下的流程相同
字节序转换
1
2
3
4
5
6
7
8
9
10//window上没有这两个函数
inet_ntop();
inet_pton();
//// 字符串IP -> 大端整形
unsigned long inet_addr (const char FAR * cp); // windows
unsigned int inet_addr (const char *cp); // linux
// 大端整形 -> 字符串IP
char* inet_ntoa(struct in_addr in);socket描述符
1
//windows下为SOCKET类型 本质与Linux下的int相同
Interface
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// 返回值: 成功返回套接字, 失败返回INVALID_SOCKET
SOCKET socket(int af,int type,int protocal);
/*
af: 地址族协议
ipv4: AF_INET (windows/linux)
PF_INET (windows)
AF_INET == PF_INET -----------------windows下的异同
type:
SOCK_STREAM
SOCK_DGRAM
protocal: 一般写0 即可
在windows上的另一种写法 -----------------windows下的异同
IPPROTO_TCP, 使用指定的流式协议中的tcp协议
IPPROTO_UDP, 使用指定的报式协议中的udp协议
return:
成功 返回套接字 / 失败 返回INVALID_SOCKET -----------------windows下的异同
*/
// 关键字: FAR NEAR, 这两个关键字在32/64位机上是没有意义的, 指定的 内存的寻址方式 -----------------windows下的异同
int bind(SOCKET s,const struct sockaddr FAR* name, int namelen);
/*
return:
成功 返回0 / 失败 返回SOCKET_ERROR -----------------windows下的异同
*/
int listen(SOCKET s,int backlog);
/*
return:
成功返回0 / 失败返回SOCKET_ERROR -----------------windows下的异同
*/
SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
/*
return:
成功 返回用于通信的套接字 / 失败返回INVALID_SOCKET
*/
int connect (SOCKET s,const struct sockaddr FAR* name,int namelen );
/*
return:
成功 返回0 / 失败返回SOCKET_ERROR
*/
//如果要使用windows api 中的 connect 需要在函数名前加::
::connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 接收数据
int recv (SOCKET s,char FAR* buf,int len,int flags);
// 发送数据
int send (SOCKET s,const char FAR * buf, int len,int flags);
// 关闭套接字
int closesocket (SOCKET s);
/*
return:
成功 返回0 / 失败 返回SOCKET_ERROR
*/
//----------------------- udp 通信函数 -------------------------
// 接收数据
int recvfrom(SOCKET s,char FAR *buf,int len,int flags,
struct sockaddr FAR *from,int FAR *fromlen);
// 发送数据
int sendto(SOCKET s,const char FAR *buf,int len,int flags,
const struct sockaddr FAR *to,int tolen);
Socket Programing - 单线程
Server
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
using namespace std;
int main() {
// 初始化套接字环境
WORD libversion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(libversion, &wsadata) != 0) {
cout << "init winsock error." << endl;
return 0;
}
if (LOBYTE(wsadata.wVersion) != 2 && HIBYTE(wsadata.wVersion) != 2) {
WSACleanup();
cout << "version error." << endl;
return 0;
}
// 创建server 监听socket描述字
SOCKET lis_socket = socket(AF_INET, SOCK_STREAM, 0);
if (lis_socket == INVALID_SOCKET) {
closesocket(lis_socket);
WSACleanup();
cout << "socket creat error." << endl;
}
//绑定到指定协议地址 (IP+Port)
sockaddr_in addr_s;
addr_s.sin_family = AF_INET;
addr_s.sin_port = htons(1234);
addr_s.sin_addr.S_un.S_addr = ADDR_ANY;
if (bind(lis_socket, (sockaddr*)&addr_s, sizeof(addr_s)) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket bind error." << endl;
}
//监听 socket描述字 的读缓冲的连接情况
if (listen(lis_socket, 128) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket listen error." << endl;
}
//阻塞 等待客户端发起请求 建立连接
SOCKET tx_socket;
sockaddr_in addr_c;
int len = sizeof(addr_c);
tx_socket = accept(lis_socket, (sockaddr*)&addr_c, &len);
if (tx_socket == INVALID_SOCKET) {
closesocket(lis_socket);
closesocket(tx_socket);
WSACleanup();
cout << "socket accept error." << endl;
}
else {
cout << "已与一客户端建立连接!" << endl << "客户端IP: " << inet_ntoa(addr_c.sin_addr) << endl << "客户端Port: " << ntohs(addr_c.sin_port) << endl;
}
while (1) {
//读取数据
char buff[1024];
memset(buff, 0, 1024); //清空buff缓冲区
int sign = recv(tx_socket, buff, 1024, 0);
if (sign > 0) {
cout << "客户端 say: " << buff << endl;
}
else if (sign == 0) {
cout << "客户端断开连接!" << endl;
break;
}
else {
cout << "数据读取失败" << endl;
break;
}
//发送数据
cout << "请输入希望发送的内容 :" << endl;
string content;
getline(cin, content);
sign = send(tx_socket, content.c_str(), strlen(content.c_str()), 0);
if (sign > 0) {
}
else if (sign == 0) {
cout << "客户端断开连接!" << endl;
break;
}
else {
cout << "数据发送失败" << endl;
break;
}
}
//关闭套接字
closesocket(lis_socket);
closesocket(tx_socket);
return 0;
}
Client
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
using namespace std;
int main() {
// 初始化套接字环境
WORD libversion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(libversion, &wsadata) != 0) {
cout << "init winsock error." << endl;
return 0;
}
if (LOBYTE(wsadata.wVersion) != 2 && HIBYTE(wsadata.wVersion) != 2) {
WSACleanup();
cout << "version error." << endl;
return 0;
}
// 创建server 通信socket描述字
SOCKET c_socket = socket(AF_INET, SOCK_STREAM, 0);
if (c_socket == INVALID_SOCKET) {
closesocket(c_socket);
WSACleanup();
cout << "socket creat error." << endl;
}
//发起连接请求
sockaddr_in addr_s;
addr_s.sin_family = AF_INET;
addr_s.sin_port = htons(1234);
addr_s.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(c_socket, (sockaddr*)&addr_s, sizeof(addr_s)) == SOCKET_ERROR) {
closesocket(c_socket);
WSACleanup();
cout << "socket connect error." << endl;
}
while (1) {
//发送数据
cout << "请输入希望发送的内容 :" << endl;
string content;
getline(cin, content);
int sign = send(c_socket, content.c_str(), strlen(content.c_str()), 0);
if (sign > 0) {
}
else if (sign == 0) {
cout << "服务端断开连接!" << endl;
break;
}
else {
cout << "数据发送失败" << endl;
break;
}
//读取数据
char buff[1024];
memset(buff, 0, 1024); //清空buff缓冲区
sign = recv(c_socket, buff, 1024, 0);
if (sign > 0) {
cout << "服务端 say: " << buff << endl;
}
else if (sign == 0) {
cout << "服务端断开连接!" << endl;
break;
}
else {
cout << "数据读取失败" << endl;
break;
}
}
}
Problem
只能处理一对连接,无法处理server与多对client的连接
Reasons
存在引起进程阻塞的函数
accept()
当监听socket描述字的读缓冲区内存无连接对象时,accept()函数将导致进程进入阻塞态
send()
当通信socket描述字的读缓冲区内存中无数据可读时,send()函数将导致进程进入阻塞态
write()
当通信socket描述字的写缓冲区内存满时,write()函数将导致进程进入阻塞态
Socket Programing - 多线程
主线程
主要负责监听
- **循环accept()**接受连接
- 每接受一个连接,创建一个子线程进行相应的通信
- 回收子线程资源,由于join()会造成accept()阻塞,因此直接使用detach()作线程分离
子线程
主要负责通信
- 基于主线程传来的文件描述符与client地址,进行数据的发送与接受
线程同步分析
主线程单独处理共有媒介数组
子线程也仅仅对收到的数据进行回复
因此不涉及多个线程对共享资源的争夺,不必做线程同步
Server
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185 //若使用c++11中的多线程,必须引入的两个库
using namespace std;
struct Socket_addr {
sockaddr_in addr;
int fd;
};
struct Socket_addr pp[512];
void* connect_thread(void* arg);
int main() {
// 初始化套接字环境
WORD libversion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(libversion, &wsadata) != 0) {
cout << "init winsock error." << endl;
return 0;
}
if (LOBYTE(wsadata.wVersion) != 2 && HIBYTE(wsadata.wVersion) != 2) {
WSACleanup();
cout << "version error." << endl;
return 0;
}
// 创建server 监听socket描述字
SOCKET lis_socket = socket(AF_INET, SOCK_STREAM, 0);
if (lis_socket == INVALID_SOCKET) {
closesocket(lis_socket);
WSACleanup();
cout << "socket creat error." << endl;
}
//绑定到指定协议地址 (IP+Port)
sockaddr_in addr_s;
addr_s.sin_family = AF_INET;
addr_s.sin_port = htons(1234);
addr_s.sin_addr.S_un.S_addr = ADDR_ANY;
//ADDR_ANY 宏定义,可以代表本机任意一个IP地址
if (::bind(lis_socket, (sockaddr*)&addr_s, sizeof(addr_s)) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket bind error." << endl;
}
//监听 socket描述字 的读缓冲的连接情况
if (listen(lis_socket, 128) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket listen error." << endl;
}
//阻塞 等待客户端发起请求 建立连接
SOCKET tx_socket;
sockaddr_in addr_c;
int len = sizeof(addr_c);
//主线程
//1. 循环接受服务端发起的连接
//2. 对应每个连接创建相应的子线程处理通信
//3. 与子线程分离,使其他线程承担子线程的资源回收工作,防止主线程受阻塞
//4. 关闭监听套接字描述符,通信套接字描述符不能由主线程关闭,而应由通信子线程关闭
//初始化参数数组
for (int i = 0; i < 512; i++) {
//-1表示无效套接字描述符,可通过判断该描述符是否有效来判断是否还可以接受新的连接,并将新的连接存入该数组
pp[i].fd = -1;
}
struct Socket_addr* p;
while (1) {
tx_socket = accept(lis_socket, (sockaddr*)&addr_c, &len);
if (tx_socket == INVALID_SOCKET) {
closesocket(lis_socket);
closesocket(tx_socket);
WSACleanup();
cout << "socket" << endl;
cout << "socket accept error." << endl;
}
else {
//设置初始化参数
for (int i = 0; i < 512; i++) {
if (pp[i].fd == -1) {
//设置参数对应的地址与通信套接字描述符信息
pp[i].addr = addr_c;
pp[i].fd = tx_socket;
//获取线程创建参数的内存,如果无为-1的,说明无法在创建更多线程了
p = &pp[i];
//至关重要的break,若忽略,处理第一次连接的线程将占据所有媒介资源
//只要第一次连接的客户端不退出,将导致能够检测到其他连接但是却不能够通信,因为数组中所有p->fd=-1
break;
}
if (i == 512 - 1)
{
//sleep(5)
this_thread::sleep_for(chrono::seconds(5));
i--;
}
}
//创建子线程
thread con(connect_thread, p);
//子线程资源分离
//不使用join回收的原因是join是阻塞函数,将导致accept所在线程的阻塞
con.detach();
}
}
//关闭监听套接字描述符
closesocket(lis_socket);
return 0;
}
void* connect_thread(void* arg) {
struct Socket_addr* p = (struct Socket_addr*)arg;
cout << "已与新客户端建立连接!" << endl << "客户端IP: " << inet_ntoa(p->addr.sin_addr) << endl << "客户端Port: " << ntohs(p->addr.sin_port) << endl;
while (1) {
//读取数据
char buff[1024];
memset(buff, 0, 1024); //清空buff缓冲区
int sign = recv(p->fd, buff, 1024, 0);
if (sign > 0) {
cout << "客户端" << inet_ntoa(p->addr.sin_addr) << "say: " << buff << endl;
}
else if (sign == 0) {
cout << "客户端"<< inet_ntoa(p->addr.sin_addr) <<"断开连接!" << endl;
p->fd = -1;
break;
}
else {
cout << "读取客户端"<< inet_ntoa(p->addr.sin_addr) <<"数据失败!" << endl;
p->fd = -1;
break;
}
//发送数据
cout << "请输入希望向客户端" << inet_ntoa(p->addr.sin_addr) <<"发送的内容 :" << endl;
string content;
getline(cin, content);
sign = send(p->fd, content.c_str(), strlen(content.c_str()), 0);
if (sign > 0) {
}
else if (sign == 0) {
cout << "客户端" << inet_ntoa(p->addr.sin_addr) << "断开连接!" << endl;
p->fd = -1;
break;
}
else {
cout << "向客户端" << inet_ntoa(p->addr.sin_addr) << "发送数据失败!" << endl;
p->fd = -1;
break;
}
}
//关闭套接字
closesocket(p->fd);
//设置相应数组位置的fd值为-1,为其他连接释放相应资源
p->fd = -1;
return NULL;
}
注意:
Winsock2.h 与 c++11中 bind()函数冲突 解决方案
- 不使用std命名空间
- 使用运算符 :: ,::bind()指明调用Winsock2.h中的bind函数
Client
1 //同单线程
Socket Programing - 线程池
Server
创建线程池对象
添加任务
- 连接任务(需要主线程传递所需要的参数)
- 通信任务(每一个连接任务对应一个通信任务,参数传递体现关联性)
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using namespace std;
struct Socket_addr {
sockaddr_in addr;
int fd;
};
//-------------------------------------------------------------------------------
//主线程向线程池中的执行线程传递执行需要的参数 : 监听套接字,线程池对象
//线程池对象参数的原因是因为执行线程处理函数是个静态方法
struct Socket_arg {
Thread_pool<Socket_arg> *p;
int fd;
};
//-------------------------------------------------------------------------------
void accept_task(void* arg);
void connect_task(void* arg);
int main() {
// 初始化套接字环境
WORD libversion = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(libversion, &wsadata) != 0) {
cout << "init winsock error." << endl;
return 0;
}
if (LOBYTE(wsadata.wVersion) != 2 && HIBYTE(wsadata.wVersion) != 2) {
WSACleanup();
cout << "version error." << endl;
return 0;
}
// 创建server 监听socket描述字
SOCKET lis_socket = socket(AF_INET, SOCK_STREAM, 0);
if (lis_socket == INVALID_SOCKET) {
closesocket(lis_socket);
WSACleanup();
cout << "socket creat error." << endl;
}
//绑定到指定协议地址 (IP+Port)
sockaddr_in addr_s;
addr_s.sin_family = AF_INET;
addr_s.sin_port = htons(1234);
addr_s.sin_addr.S_un.S_addr = ADDR_ANY;
//ADDR_ANY 宏定义,可以代表本机任意一个IP地址
if (::bind(lis_socket, (sockaddr*)&addr_s, sizeof(addr_s)) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket bind error." << endl;
}
//监听 socket描述字 的读缓冲的连接情况
if (listen(lis_socket, 128) == SOCKET_ERROR) {
closesocket(lis_socket);
WSACleanup();
cout << "socket listen error." << endl;
}
//-------------------------------------------------------------------------------
//创建线程池
Thread_pool<Socket_arg> pool(3, 400);
//初始化参数
//必须堆上内存,栈上内存会被共享(传递的是地址),从而后续线程通信套接字更新旧线程套接字
Socket_arg *arg = new Socket_arg();
arg->fd = lis_socket;
arg->p = &pool;
//添加任务
pool.addTask(accept_task, arg);
this_thread::sleep_for(chrono::seconds(1000000));
//-------------------------------------------------------------------------------
}
void accept_task(void* arg)
{
Socket_arg* pool = static_cast<Socket_arg*>(arg);
//阻塞 等待客户端发起请求 建立连接
SOCKET tx_socket;
sockaddr_in addr_c;
int len = sizeof(addr_c);
while (1) {
tx_socket = accept(pool->fd, (sockaddr*)&addr_c, &len);
if (tx_socket == INVALID_SOCKET) {
closesocket(pool->fd);
closesocket(tx_socket);
WSACleanup();
cout << "socket accept error." << endl;
break;
}
else {
//-------------------------------------------------------------------------------
//初始化参数
Socket_addr *a = new Socket_addr();
a->addr = addr_c;
a->fd = tx_socket;
//添加连接任务
pool->p->addTask(connect_task, a);
//-------------------------------------------------------------------------------
}
}
//关闭监听套接字描述符
closesocket(pool->fd);
}
void connect_task(void* arg) {
struct Socket_addr* p = (struct Socket_addr*)arg;
cout << "已与新客户端建立连接!" << endl << "客户端IP: " << inet_ntoa(p->addr.sin_addr) << endl << "客户端Port: " << ntohs(p->addr.sin_port) << endl;
while (1) {
//读取数据
char buff[1024];
memset(buff, 0, 1024); //清空buff缓冲区
int sign = recv(p->fd, buff, 1024, 0);
if (sign > 0) {
cout << "客户端" << ntohs(p->addr.sin_port) << "say: " << buff << endl;
}
else if (sign == 0) {
cout << "客户端" << ntohs(p->addr.sin_port) << "断开连接!" << endl;
break;
}
else {
cout << "读取客户端" << ntohs(p->addr.sin_port) << "数据失败!" << endl;
break;
}
//发送数据
cout << "请输入希望向客户端" << ntohs(p->addr.sin_port) << "发送的内容 :" << endl;
string content;
getline(cin, content);
sign = send(p->fd, content.c_str(), strlen(content.c_str()), 0);
if (sign > 0) {
}
else if (sign == 0) {
cout << "客户端" << ntohs(p->addr.sin_port) << "断开连接!" << endl;
break;
}
else {
cout << "向客户端" << ntohs(p->addr.sin_port) << "发送数据失败!" << endl;
break;
}
}
//关闭套接字
closesocket(p->fd);
}