网络编程套接字-----实现网络间通信
꧁ 大家好,我是 兔7 ,一位努力学习C++的博主~ ꧂
☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧
🚀 如有不懂,可以随时向我提问,我会全力讲解~💬
🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!👀
🔥 你们的支持是我创作的动力!⛅
🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!⭐
🧸 人的心态决定姿态!⭐
🚀 本文章CSDN首发!✍
目录
0. 前言
1. 预备知识
1. 1 理解源IP地址和目的IP地址
1.2 认识端口号
1.3 理解 "端口号" 和 "进程ID"
1.4 理解源端口号和目的端口号
1.5 IP地址、MAC地址、端口号的区别?
1.6 认识TCP协议
1.7 认识UDP协议
1.8 网络字节序
2. socket编程接口
2.1 socket 常见API
2.2 sockaddr结构
sockaddr 结构
sockaddr_in 结构
in_addr结构
3. 简单的UDP网络程序
3.1 接口
3.1.1 初始化的接口
3.1.2 运行起来需要的接口
3.2 实现 UDP
udp_server.hpp
udp_server.cc
udp_client.hpp
udp_client.cc
3.3 UDP总代码
udp_server.hpp
udp_server.cc
udp_client.hpp
udp_client.cc
4. 多方法TCP通信
4.1 接口
4.2 TCP 单执行流
tcp_server.hpp
tcp_server.cc
tcp_client.hpp
tcp_client.cc
4.3 TCP 多进程
tcp_server.hpp
tcp_server.cc
tcp_client.hpp
tcp_client.cc
4.4 TCP 多线程
tcp_server.hpp
tcp_server.cc
tcp_client.hpp
tcp_client.cc
4.5 TCP 多线程(线程池版本)
ThreadPool.hpp
Task.hpp
tcp_server.hpp
tcp_server.cc
tcp_client.hpp
tcp_client.cc
5. 零碎知识点
5.1 地址转换函数
5.2 关于inet_ntoa
0. 前言
此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。
大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~
感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!
1. 预备知识
1. 1 理解源IP地址和目的IP地址
在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。
就像唐僧经常说:贫僧从东土大唐而来,到西天求取真经。
其中"东土大唐"和"西天"就是源IP地址和目的IP地址。所以源IP地址和目的IP地址基本是不变的。
比方说上一次经过了女儿国,再往黑风岭走,唐僧可能就会说,我上一站是女儿国,我现在在往黑风岭走。
其中的地址就是源MAC地址和目的MAC地址,它在唐僧一路上都是会发生变化的,在计算机中之所以MAC地址会变化是因为在传输中会经过路由,从而进行解包和封包。
1.2 认识端口号
端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
唐僧到西天不是为了去西天而去西天,而是为了到西天拿到经书,所以西天是唐僧必须到的地方。
所以我们把数据从一台主机送到另一台主机没有意义,最终我们是想请求到对应的数据。所以我们的请求不只是要发送到对方的机器上,而是为了访问到这个机器上一个特定的服务。
所以 socket 通信,本质上是进程间通信,而且可以是跨网络的进程间通信。
那么:
- 当数据来到对端的网络层,它怎么知道要访问服务端的哪个服务呢?(当然像前面搜索引擎这么重要的服务,一台主机上就只有一个服务)
- 传递回来的时候又是要传给哪个服务呢?
所以这里就用到了端口号。
任何的网络服务与网络客户端,如果要进行正常的数据通信,必须要用端口号来唯一标识。在同一个OS内,一个进程可以与一个端口号进行绑定,该端口号就在网络层面唯一表示一台主机上的唯一一个进程。
- 公网IP:唯一的标识全网内唯一的一台主机。
- port:表示一台主机上的唯一一个进程。
所以 IP+port 就是标识全网内唯一的一个进程。
所以两端都有对应的 IP+port ,那么也就是唯二的两个进程之间进行通信。
所以我们就将 IP+port 这种标识进程间通信的方式就叫做 socket通信。
1.3 理解 "端口号" 和 "进程ID"
我在前面写的系统编程的时候,我们学习了 pid 表示唯一一个进程。此处我们的端口号也是唯一表示一个进程。
那么这两者之间是怎样的关系?
首先,一台机器上可能存在大量的进程,但不是所有的进程都要对外进行网络数据请求。
所以其实存在大量的进程是系统的概念,而进程进行网络数据请求是网络级的概念。
例如:
在学校中有自己的学号,在社会上有自己的身份证号,在公司又有自己的工号,都可以通过这些来标识自己,它们之间互不冲突,不同的地方用不同的号,是为了方便管理或者识别。
所以这里的端口号和进程ID也是如此,这也是为什么网络中单独出现个 port 的原因,因为 port 更适合。
IP地址和端口号它们之间的关系?
例如:
手机欠费了,手机只能打 10086,这时是一个人工客服接的电话提供服务。
那么这里的 10086 就相当于IP地址,而为我们提供人工客服的那个员工的工号就相当于端口号。
1.4 理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的,要发给谁"。
所以这时唐僧应该说的是:贫僧奉唐皇帝之令从东土大唐而来,到西天求取真经。
1.5 IP地址、MAC地址、端口号的区别?
- mac地址是在数据链路层包裹在以太网头部中的,它主要用来识别同一个链路中的不同计算机。Mac地址即网卡号,每块网卡出厂的时候,都有一个全世界独一无二的 MAC 地址,长度是 48 个二进制位,通常用 12 个十六进制数表示。
- IP地址是在网络层的IP头部里,用于识别网络中互联的主机和路由器,其实主要是确认子网,通过子网掩码确认某个IP地址所在的子网,而后再在子网内部确认mac地址就能找到准确的用户了。
- 端口号是在运输层包含在TCP/UDP头部中的,用于识别应用程序。一台主机上能运行多个程序,那么接收到的消息到底是哪个程序发送的,就需要端口号来确认。
1.6 认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识。后面我们再详细讨论TCP的一些细节问题。
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
1.7 认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识。后面再详细讨论。
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.8 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。
如果不注意大小端的问题,那么可能写和读的时候都会读反。所以网络必须解决大小端的问题。
那么如何解决呢?
规定网络序列:网路上的数据必须都是大端。
例如从 client 到网络要以大端的方式传输,从网络到 server ,如果 server 是大端机器就之间向上解释,如果是小端就反过来向上解释就可以了。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。
- 这些函数名很好记,h 表示 host,n 表示network,l 表示 32 位长整数,s 表示 16 位短整数。
- 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
2. socket编程接口
2.1 socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
2.2 sockaddr结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
- IPv4 和 IPv6 的地址格式定义在netinet/in.h中,IPv4 地址用 sockaddr_in 结构体表示,包括16位地址类型,16 位端口号和 32 位 IP 地址。
- IPv4、IPv6地址类型分别定义为常数 AF_INET、AF_INET6。这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容。
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in。这样的好处是程序的通用性,可以接收IPv4, IPv6,以及UNIX Domain Socket 各种类型的sockaddr 结构体指针做为参数。
sockaddr 结构
sockaddr_in 结构
虽然 socket api 的接口是sockaddr,但是我们真正在基于 IPv4 编程时,使用的数据结构是sockaddr_in。这个结构里主要有三部分信息:地址类型, 端口号,IP 地址。
in_addr结构
in_addr 用来表示一个 IPv4 的 IP 地址。其实就是一个32位的整数。
3. 简单的UDP网络程序
3.1 接口
3.1.1 初始化的接口
3.1.2 运行起来需要的接口
3.2 实现 UDP
udp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10
11
12 #define DEFAULT 8081
13
14 class UdpServer{
15 private:
16 short port;
17 int sockfd;
18 std::string ip;
19 public:
20 UdpServer(std::string _ip, int _port = DEFAULT)
21 : port(_port)
22 , sockfd(-1)
23 , ip(_ip)
24 {}
25
26 bool InitUdpServer()
27 {
28 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
29 if(sockfd < 0){
30 std::cerr << "socket error" << std::endl;
31 return false;
32 }
33 std::cout << "socket create success, sockfd: " << sockfd << std::endl;//大概率是3
34
35
36 struct sockaddr_in local;
37 memset(&local, '\0', sizeof(local));
38 local.sin_family = AF_INET;
39 local.sin_port = htons(port);
40 local.sin_addr.s_addr = inet_addr(ip.c_str());
41
42 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){
43 std::cerr << "bind error" << std::endl;
44 return false;
45 }
46 std::cout << "bind success" << std::endl;
47
48
49 return true;
50 }
51
52 void Start()
53 {
54 #define SIZE 128
55 char buffer[SIZE];
56 for( ; ; ){
57 struct sockaddr_in peer;
58 socklen_t len = sizeof(peer);
59 ssize_t size = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
60 if(size > 0){
61 buffer[size] = 0;
62 short _port = ntohs(peer.sin_port);
63 std::string _ip = inet_ntoa(peer.sin_addr);
64 std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
65 }
66 else{
67 std::cerr << "recvfrom error" << std::endl;
68 }
69 }
70 }
71
72 ~UdpServer()
73 {}
74 };
现在只写了服务器的端口,因为没有写用户的,但是我们想看一下我们可以不可以让别人访问到:
netstat 用来查看网络状态:
- -n:能显示出数字就显示出数字
- -l:列表
- -t -u:一个是tcp 一个是udp
- -p:查看进程相关的信息
Local Address 是 IP 地址和端口号。
Foreign Address 是对端的 IP 地址和端口号,0.0.0.0:* 表示的是任意的 IP,任意的端口都可以给我发送信息。
最后面的就是 PID 和程序的名字。
那么接下来我们就需要写 client 了。
客户端需要port么?客户端需要绑定么?
当然需要 port ,因为这里叫做网络编程、套接字编程,那么客户端和服务器都需要端口,但是客户端是不需要绑定的,在讲解这个问题前先说一下:
服务器为何要bind一个端口:服务器是为了给别人提供服务的,别人一定要知道你的 ip 和端口,所以端口一定要是一个众所周知的端口,而且 bind 之后,不能轻易改变,这也是为什么服务器需要 bind 一个端口,因为只有 bind 了之后这个端口才真正的属于它而不属于别人。
那么现在再为什么客户端不需要绑定:这里是不需要,而不是不能。但是一般情况下是不绑定的,因为客户端访问服务器,端口只要是唯一的即可,不需要和特定 client 进程强相关,client 端口可以动态的设置,要说绑定的话会有很多问题,比方说 bind 的是 8088,如果 8088 被别人占用了,那么这个客户端就启动不起来。
所以客户端只需要调用该 sendto 类似的接口,client 直接在 OS 层面会自动给 client 获取一个唯一的端口。
udp_server.cc
1 #include "udp_server.hpp"
2
3 //./udp_server port
4
5 int main(int argc, char* argv[])
6 {
7 if(argc != 2){
8 std::cerr << "Usage: " << argv[0] << " port" << std::endl;
9 return 1;
10 }
11
12 std::string ip = "127.0.0.1"; //127.0.0.1 == localhost : 标识本机器 : 本地环回
13 int port = atoi(argv[1]);
14
15 UdpServer* svr = new UdpServer(ip, port);
16 svr->InitUdpServer();
17
18 svr->Start();
19 return 0;
20 }
udp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10
11
12 class UdpClient{
13 private:
14 int sockfd;
15 std::string ip;
16 int port;
17 public:
18 UdpClient(std::string _ip, int _port)
19 :ip(_ip)
20 , port(_port)
21 {}
22 bool InitUdpClient()
23 {
24 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
25 if(sockfd < 0){
26 std::cerr << "socket error!" << std::endl;
27 return false;
28 }
29 return true;
30 }
31 void Start()
32 {
33 struct sockaddr_in peer;
34 memset(&peer, 0, sizeof(peer));
35 peer.sin_family = AF_INET;
36 peer.sin_port = htons(port);
37 peer.sin_addr.s_addr = inet_addr(ip.c_str());
38
39 std::string msg;
40 for(; ; ){
41 std::cout << "Pleace Enter# ";
42 std::cin >> msg;
43 sendto(sockfd, msg.c_str(), msg.size(), 0,(struct sockaddr*) &peer, sizeof(peer));
44 }
45
46
47 }
48 ~UdpClient()
49 {}
50 };
udp_client.cc
1 #include "udp_client.hpp"
2
3 //./udp_cilent ip port
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cerr << "Usage: " << argv[0] << "ip port" << std::endl;
8 return 1;
9 }
10 std::string ip = argv[1];
11 int port = atoi(argv[2]);
12 UdpClient* uclt = new UdpClient(ip, port);
13 uclt->InitUdpClient();
14
15 uclt->Start();
16
17 return 0;
18 }
因为这里我们绑定的不是外网,而是本地环回,我们接下来通信一下:
我们可以看到,我们完成了通信。
我们可以通过网络工具查看到这两个服务器。
那么如果我们的 IP 地址绑定的是公网 IP ,那么我们的 udp_server 就可以被外网访问呢?
那么此时我换上我的公网 IP 试试。
我们可以看到每次 bind 都是失败的,所以:
其实云服务器的 ip,是由对应的云厂商提供的,这个 ip 并不一定是真正的公网 ip,当然有可能这个云服务器收到公网 ip 再做一下转发的工作,但是现在不管这个。
所以这个 ip 不能直接被绑定,如果需要 bind,需要让外网访问,需要 bind 0 ,INADDR_ANY -> 0 ,这个就是个宏,这个宏是 0 。 bind 0 意味着服务器可以接收来自任何 client 的请求。
但是有没有可能一个服务器有多张网卡呢?当然是有可能,那么当然也就有多个 ip。
所以 INADDR_ANY 也是强烈推荐使用的。
所以我们其实就不需要去写 ip。
我们可以看到,前面的是 127.0.0.1 ,此时的就是 0.0.0.0,也就意味着我们可以在本地读取任何一张网卡里的数据,只要能发过去,就能接收。
我们现在只做了读取,那么如果想要再发送回去呢?
那么接下来就写一个回声服务器,就是客户端发什么数据,服务器就给返回去。
我们只要加上上面红框的内容就可以,其实也就是再 server 里添加一个发送传过来的字符串的接口,然后再让 client 接收 server 传过来的字符串再打印下来就可以了。
接下来就写一个简单的业务:
我们只在 udp_server 中创建一个子进程,让子进程去完成这个业务就好。
我们可以看到,客户端输入 ls,服务端也完成了对应的业务。
那么接下来想要再回显给客户端:
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10 #include <unistd.h>
11 #include <sys/wait.h>
12 #define DEFAULT 8081
13
14 class UdpServer{
15 private:
16 int port;
17 int sockfd;
18 //std::string ip;
19 public:
20 UdpServer(int _port = DEFAULT)
21 : port(_port)
22 , sockfd(-1)
23 {}
24
25 bool InitUdpServer()
26 {
27 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
28 if(sockfd < 0){
29 std::cerr << "socket error" << std::endl;
30 return false;
31 }
32 std::cout << "socket create success, sockfd: " << sockfd << std::endl;//大概率是3
33
34
35 struct sockaddr_in local;//网络通信地址
36 memset(&local, '\0', sizeof(local));
37 local.sin_family = AF_INET;
38 local.sin_port = htons(port);//端口号
39 //local.sin_addr.s_addr = inet_addr(ip.c_str());
40 local.sin_addr.s_addr = INADDR_ANY;
41 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){
42 std::cerr << "bind error" << std::endl;
43 return false;
44 }
45 std::cout << "bind success" << std::endl;
46
47
48 return true;
49 }
50
51 void Start()
52 {
53 #define SIZE 128
54 char buffer[SIZE];
55
56 for( ; ; ){
57 struct sockaddr_in peer;
58 socklen_t len = sizeof(peer);
59 ssize_t size = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
60 if(size > 0){
61 buffer[size] = 0;
62 int _port = ntohs(peer.sin_port);
63 std::string _ip = inet_ntoa(peer.sin_addr);
64 std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
65
66 std::string cmd = buffer;
67 std::string result;
68 if(cmd == "ls"){
69 int pipes[2];
70 pipe(pipes);
71
72 pid_t id = fork();
73 if(id == 0){
74 close(pipes[0]);//子进程要写,所以关闭读
75 dup2(pipes[1], 1);
76 execl("/usr/bin/ls", "ls", "-a", "-l", "-i", nullptr);
77 exit(1);
78 }
79 close(pipes[1]);//父进程要读,所以要关闭写
80 char c;
81 while(1){
82 if(read(pipes[0], &c, 1) > 0){
83 result.push_back(c);
84 }
85 else{
86 break;
87 }
88 }
89 wait(nullptr);
90 }
91
92 std::string echo_msg;
93 if(result.empty()){
94 echo_msg = "server get!->";
95 echo_msg += buffer;
96 }
97 else{
98 echo_msg = result;
99 }
100 sendto(sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);
101 }
102 else{
103 std::cerr << "recvfrom error" << std::endl;
104 }
105 }
106 }
107
108 ~UdpServer()
109 {
110 if(sockfd >= 0){
111 close(sockfd);
112 }
113 }
114 };
这里的思路是:如果输入的是 ls,那么创建一个匿名管道,然后子进程要写,所以关闭读,父进程要读,所以关闭写,因为管道只能单向通信,然后父进程将读到的写到 result 字符串里,然后打印到客户端。
如果输入的不是 ls,那么就打印到客户端是你输入的内容。
这里效果其实有点问题,我们可以看到我们打印的是 ls -a -l -i,他应该展示出来的要比这个多,这应该是因为管道是有大小的,所以 execl 的时候已经都将管道占满了还没有打印完,所以出现这个情况,但是如果我们打印 ls 的话就是这样的:
我们可以看到这样是打印全的,所以应该就是上面所说的问题~
此时我们的 UDP 就会用了,下面给出了代码,大家可以也去试试。
3.3 UDP总代码
udp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10 #include <unistd.h>
11 #include <sys/wait.h>
12 #define DEFAULT 8081
13
14 class UdpServer{
15 private:
16 int port;
17 int sockfd;
18 //std::string ip;
19 public:
20 UdpServer(int _port = DEFAULT)
21 : port(_port)
22 , sockfd(-1)
23 {}
24
25 bool InitUdpServer()
26 {
27 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
28 if(sockfd < 0){
29 std::cerr << "socket error" << std::endl;
30 return false;
31 }
32 std::cout << "socket create success, sockfd: " << sockfd << std::endl;//大概率是3
33
34
35 struct sockaddr_in local;//网络通信地址
36 memset(&local, '\0', sizeof(local));
37 local.sin_family = AF_INET;
38 local.sin_port = htons(port);//端口号
39 //local.sin_addr.s_addr = inet_addr(ip.c_str());
40 local.sin_addr.s_addr = INADDR_ANY;
41 if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0){
42 std::cerr << "bind error" << std::endl;
43 return false;
44 }
45 std::cout << "bind success" << std::endl;
46
47
48 return true;
49 }
50
51 void Start()
52 {
53 #define SIZE 1000
54 char buffer[SIZE];
55
56 for( ; ; ){
57 struct sockaddr_in peer;
58 socklen_t len = sizeof(peer);
59 ssize_t size = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
60 if(size > 0){
61 buffer[size] = 0;
62 int _port = ntohs(peer.sin_port);
63 std::string _ip = inet_ntoa(peer.sin_addr);
64 std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
65
66 std::string cmd = buffer;
67 std::string result;
68 if(cmd == "ls"){
69 int pipes[2];
70 pipe(pipes);
71
72 pid_t id = fork();
73 if(id == 0){
74 close(pipes[0]);//子进程要写,所以关闭读
75 dup2(pipes[1], 1);
76 execl("/usr/bin/ls", "ls", nullptr);
77 exit(1);
78 }
79 close(pipes[1]);//父进程要读,所以要关闭写
80 char c;
81 while(1){
82 if(read(pipes[0], &c, 1) > 0){
83 result.push_back(c);
84 }
85 else{
86 break;
87 }
88 }
89 wait(nullptr);
90 }
91
92 std::string echo_msg;
93 if(result.empty()){
94 echo_msg = "server get!->";
95 echo_msg += buffer;
96 }
97 else{
98 echo_msg = result;
99 }
100 sendto(sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);
101 }
102 else{
103 std::cerr << "recvfrom error" << std::endl;
104 }
105 }
106 }
107
108 ~UdpServer()
109 {
110 if(sockfd >= 0){
111 close(sockfd);
112 }
113 }
114 };
udp_server.cc
1 #include "udp_server.hpp"
2
3 //./udp_server port
4
5 int main(int argc, char* argv[])
6 {
7 if(argc != 2){
8 std::cerr << "Usage: " << argv[0] << " port" << std::endl;
9 return 1;
10 }
11
12 //std::string ip = "127.0.0.1"; //127.0.0.1 == localhost : 标识本机器 : 本地环回
13 //std::string ip = "49.232.134.146";
14 int port = atoi(argv[1]);
15
16 UdpServer* svr = new UdpServer(port);
17 svr->InitUdpServer();
18
19 svr->Start();
20 return 0;
21 }
udp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <netinet/in.h>
10 #include <unistd.h>
11
12 class UdpClient{
13 private:
14 int sockfd;
15 std::string ip;
16 int port;
17 public:
18 UdpClient(std::string _ip, int _port)
19 :ip(_ip)
20 , port(_port)
21 {}
22 bool InitUdpClient()
23 {
24 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
25 if(sockfd < 0){
26 std::cerr << "socket error!" << std::endl;
27 return false;
28 }
29 return true;
30 }
31 void Start()
32 {
33 struct sockaddr_in peer;
34 memset(&peer, 0, sizeof(peer));
35 peer.sin_family = AF_INET;
36 peer.sin_port = htons(port);
37 peer.sin_addr.s_addr = inet_addr(ip.c_str());
38
39 std::string msg;
40 for(; ; ){
41 std::cout << "Pleace Enter# ";
42 std::cin >> msg;
43 sendto(sockfd, msg.c_str(), msg.size(), 0,(struct sockaddr*) &peer, sizeof(peer));
44
45 char buffer[128];
46 //当然如果担心客户端发生变化就可以把peer传过来
47 struct sockaddr_in temp;
48 socklen_t len = sizeof(temp);
49 ssize_t size = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
50 if(size > 0){
51 buffer[size] = 0;
52 std::cout << buffer << std::endl;
53 }
54 }
55
56
57 }
58 ~UdpClient()
59 {
60 if(sockfd >= 0){
61 close(sockfd);
62 }
63 }
64 };
udp_client.cc
1 #include "udp_client.hpp"
2
3 //./udp_cilent ip port
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cerr << "Usage: " << argv[0] << "ip port" << std::endl;
8 return 1;
9 }
10 std::string ip = argv[1];
11 int port = atoi(argv[2]);
12 UdpClient* uclt = new UdpClient(ip, port);
13 uclt->InitUdpClient();
14
15 uclt->Start();
16
17 return 0;
18 }
4. 多方法TCP通信
4.1 接口
4.2 TCP 单执行流
因为 tcp 是面向连接的,所以需要在正式发送数据之前,先要建立链接。既然要建立连接,那么服务器端必须得不断的花时间进行检测是否有新的链接到来。
tcp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10
11 #define BACKLOG 5
12
13 class TcpServer{
14 private:
15 short port;
16 int listen_sock;
17 public:
18 TcpServer(short _port)
19 :port(_port)
20 ,listen_sock(-1)
21 {}
22 bool InitTcpServer()
23 {
24 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
25 if(listen_sock < 0){
26 std::cerr << "socket error" << std::endl;
27 return false;
28 }
29
30 struct sockaddr_in local;
31 //bzero(&local, sizeof(local));
32 memset(&local, 0, sizeof(local));
33 local.sin_family = AF_INET;
34 local.sin_port = htons(port);
35 local.sin_addr.s_addr = INADDR_ANY;
36
37 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
38 std::cerr << "bind error" << std::endl;
39 return false;
40 }
41
42 if(listen(listen_sock, BACKLOG) < 0){
43 std::cerr << "listen error" <<std::endl;
44 return false;
45 }
46 std::cout << "listen success" << std::endl;
47 return true;
48 }
49 void Loop()
50 {
51 struct sockaddr_in peer;
52 for(;;)
53 {
54 //先获取链接
55 socklen_t len = sizeof(peer);
56 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
57 if(sock < 0){
58 std::cerr << "accept error" << std::endl;
59 continue;
60 }
61 std::cout << "get a new link: " << sock << std::endl;
62 }
63 }
64 ~TcpServer()
65 {}
66 };
tcp_server.cc
1 #include "tcp_server.hpp"
2
3 int main()
4 {
5 TcpServer tsvr(8081);
6 tsvr.InitTcpServer();
7
8 tsvr.Loop();
9
10
11 return 0;
12 }
listen_sock:获取新链接。
sock:服务新链接(读取数据、分析数据、处理数据、写入数据)。
这里有一个命令 telnet 是远程登陆。
我们可以看到,用本地环回可以建立链接,用公网 ip 也可以。
甚至,我们还可以用浏览器去访问 8081,虽然服务器里现在什么都没有,但是浏览器底层用的是 http,或者 https ,底层都是 tcp,所以它也会向服务器发送请求。
tcp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <unistd.h>
7 #include <sys/types.h>
8 #include <sys/socket.h>
9 #include <arpa/inet.h>
10 #include <netinet/in.h>
11
12 class TcpClient{
13 private:
14 std::string ip;
15 short port;
16 int sock;
17 public:
18 TcpClient(std::string _ip, short _port)
19 :ip(_ip)
20 ,port(_port)
21 ,sock(-1)
22 {}
23 void InitTcpClient()
24 {
25 sock = socket(AF_INET, SOCK_STREAM, 0);
26 if(sock < 0){
27 std::cerr << "socket error" << std::endl;
28 exit(2);
29 }
30
31 }
32
33 void Loop()
34 {
35 struct sockaddr_in peer;
36 memset(&peer, 0, sizeof(peer));
37 peer.sin_family = AF_INET;
38 peer.sin_port = htons(port);
39 peer.sin_addr.s_addr = inet_addr(ip.c_str());;
40 //连接服务器
41 if(connect(sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){
42 //success
43 std::cout << "connect success ..." << std::endl;
44 Request(sock);
45 }
46 else{
47 //failed
48 std::cerr << "connect failed ..." << std::endl;
49 }
50 }
51
52 void Request(int sock)
53 {
54 std::string msg;
55 char buffer[1024];
56 while(true){
57 std::cout << "Please Entet# ";
58 std::cin >> msg;
59
60 write(sock, msg.c_str(), msg.size());
61
62 ssize_t s = read(sock, buffer, sizeof(buffer)-1);
63 if(s > 0){
64 buffer[s] = 0;
65 }
66 std::cout << "server echo# " << buffer << std::endl;
67 }
68 }
69
70 ~TcpClient()
71 {
72 if(sock >= 0){
73 close(sock);
74 }
75 }
76 };
tcp_client.cc
1 #include "tcp_client.hpp"
2
3
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cout << "Usage: " << argv[0] << "ip port" << std::endl;
8 exit(1);
9 }
10
11 TcpClient tclt(argv[1], atoi(argv[2]));
12 tclt.InitTcpClient();
13
14 tclt.Loop();
15
16 return 0;
17 }
我们可以看到,我们是可以进行通信的,而且当我们关掉客户端的时候,服务就会终止,但是服务器还是在运行的,它再等下一个服务请求。
但是这里有个问题:
当我们两个客户端同时链接,同时发消息就会出现这个情况,然后:
当我们 ctrl+c 的时候 42424 退出了,又来了个链接 42436,然后 "你是好人" 也发送了,
所以这里现在其实是串行的,因为我们现在写的这个是一个单执行流的服务器,现在只能处理一个人的请求。
4.3 TCP 多进程
tcp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10 #include <signal.h>
11 #include <sys/wait.h>
12 #define BACKLOG 5
13
14 class TcpServer{
15 private:
16 short port;
17 int listen_sock;
18 public:
19 TcpServer(short _port)
20 :port(_port)
21 ,listen_sock(-1)
22 {}
23 bool InitTcpServer()
24 {
25 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
26 if(listen_sock < 0){
27 std::cerr << "socket error" << std::endl;
28 return false;
29 }
30
31 struct sockaddr_in local;
32 //bzero(&local, sizeof(local));
33 memset(&local, 0, sizeof(local));
34 local.sin_family = AF_INET;
35 local.sin_port = htons(port);
36 local.sin_addr.s_addr = INADDR_ANY;
37
38 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
39 std::cerr << "bind error" << std::endl;
40 return false;
41 }
42
43 if(listen(listen_sock, BACKLOG) < 0){
44 std::cerr << "listen error" <<std::endl;
45 return false;
46 }
47 std::cout << "listen success" << std::endl;
48 return true;
49 }
50 void Loop()
51 {
52 //这样写父进程就不用等子进程了,很推荐,因为很简单
53 //signal(SIGCHLD, SIG_IGN);
54 struct sockaddr_in peer;
55 for(;;)
56 {
57 //先获取链接
58 socklen_t len = sizeof(peer);
59 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
60 //这里是accept失败
61 if(sock < 0){
62 std::cerr << "accept error" << std::endl;
63 continue;
64 }
65
66
67 pid_t id = fork();
68 if(id == 0){
69 //创建出来孙子进程是为了土共服务的
70 //所以不用关心 listen_sock了
71 close(listen_sock);//这里不是必要的
72 if(fork() > 0){
73 exit(0);
74 }
75 //孙子进程
76 //这样做的原因是父进程退出,子进程被系统领养成为孤儿进程
77 std::string ip = inet_ntoa(peer.sin_addr);
78 int port = ntohs(peer.sin_port);
79 std::cout << "get a new link:"<< sock <<"[" << ip << "]" << port << std::endl;
80 Service(sock, ip, port);//处理
81 exit(0);
82 }
83
84 //这里关掉sock不会影响孙子进程的使用
85 //因为就相当于爷爷进程将指向sock这个文件描述符关掉
86 close(sock);//这里是必要的
87 //这里还是要等的,因为父进程退出,爷爷进程是要等父进程的
88 //但是这里就不算是挂起了,因为父进程是直接就退出,爷爷进程直接回收
89 waitpid(id, nullptr, 0);//这里的id就是父进程的id,当然这里也可以写成-1
90 }
91
92 }
93
94 void Service(int sock, std::string& ip, short port)
95 {
96 char buffer[1024];
97 while(true){
98 ssize_t size = read(sock, buffer, sizeof(buffer)-1);
99 if(size > 0){
100 buffer[size] = 0;
101 std::cout << ip << ":" << port << "# " << buffer << std::endl;
102 //回显
103 write(sock, buffer, size);
104 }
105 else if (size == 0){
106 std::cout << ip << ":" << port << "close!" << buffer << std::endl;
107 break;
108 }
109 else{
110 std::cerr << port << "read error" << std::endl;
111 break;
112 }
113 }
114
115 close(sock);
116 std::cout << "Service done!" << std::endl;
117 }
118
119 ~TcpServer()
120 {
121 if(listen_sock >= 0){
122 close(listen_sock);
123 }
124 }
125 };
tcp_server.cc
1 #include "tcp_server.hpp"
2
3 int main(int argc, char* argv[])
4 {
5 if(argc != 2){
6 std::cout << "Usage: " << argv[0] << "port" << std::endl;
7 exit(1);
8 }
9 TcpServer tsvr(atoi(argv[1]));
10 tsvr.InitTcpServer();
11
12 tsvr.Loop();
13
14
15 return 0;
16 }
tcp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <unistd.h>
7 #include <sys/types.h>
8 #include <sys/socket.h>
9 #include <arpa/inet.h>
10 #include <netinet/in.h>
11
12 class TcpClient{
13 private:
14 std::string ip;
15 short port;
16 int sock;
17 public:
18 TcpClient(std::string _ip, short _port)
19 :ip(_ip)
20 ,port(_port)
21 ,sock(-1)
22 {}
23 void InitTcpClient()
24 {
25 sock = socket(AF_INET, SOCK_STREAM, 0);
26 if(sock < 0){
27 std::cerr << "socket error" << std::endl;
28 exit(2);
29 }
30
31 }
32
33 void Loop()
34 {
35 struct sockaddr_in peer;
36 memset(&peer, 0, sizeof(peer));
37 peer.sin_family = AF_INET;
38 peer.sin_port = htons(port);
39 peer.sin_addr.s_addr = inet_addr(ip.c_str());;
40 //连接服务器
41 if(connect(sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){
42 //success
43 std::cout << "connect success ..." << std::endl;
44 Request(sock);
45 }
46 else{
47 //failed
48 std::cerr << "connect failed ..." << std::endl;
49 }
50 }
51
52 void Request(int sock)
53 {
54 std::string msg;
55 char buffer[1024];
56 while(true){
57 std::cout << "Please Entet# ";
58 std::cin >> msg;
59
60 write(sock, msg.c_str(), msg.size());
61
62 ssize_t s = read(sock, buffer, sizeof(buffer)-1);
63 if(s > 0){
64 buffer[s] = 0;
65 }
66 std::cout << "server echo# " << buffer << std::endl;
67 }
68 }
69
70 ~TcpClient()
71 {
72 if(sock >= 0){
73 close(sock);
74 }
75 }
76 };
tcp_client.cc
1 #include "tcp_client.hpp"
2
3
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cout << "Usage: " << argv[0] << "ip port" << std::endl;
8 exit(1);
9 }
10
11 TcpClient tclt(argv[1], atoi(argv[2]));
12 tclt.InitTcpClient();
13
14 tclt.Loop();
15
16 return 0;
17 }
这里其实只在 tcp_server.hpp 中添加了这个,这里的文字很重要,大家根据步骤看看,应该是可以看懂的,写的解析很全面。
这里的理解非常重要,但是我要说的是,其实这里实际写不用这么复杂,因为这只是一个很好玩的方法,但是有比这个简单很多很多的,就是直接:
因为上面写那么多的目的就是为了让子进程单独去完成任务,这样写直接就不用等子进程了,所以这个是最推荐,或者说是最好的~!
首先我们通过右边的监视脚本看到从一个进程到三个进程,然后随着退出从三个进程到一个进程。
而且我们连接服务器的时候它的文件描述符都是 4 ,这是因为我们是一个一个连的,连玩之后爷爷进程就会关闭 sock 文件描述符,所以就一直是 4 ,但是如果一次性来多个客户端,可能会出现 4、5、6......。
而且我们也可以看到我们两个进程是直接都连上了客户端,而且服务器端都会给客户端提供 服务。
4.4 TCP 多线程
tcp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10 #include <signal.h>
11 #include <sys/wait.h>
12 #define BACKLOG 5
13
14 class TcpServer{
15 private:
16 short port;
17 int listen_sock;
18 public:
19 TcpServer(short _port)
20 :port(_port)
21 ,listen_sock(-1)
22 {}
23 bool InitTcpServer()
24 {
25 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
26 if(listen_sock < 0){
27 std::cerr << "socket error" << std::endl;
28 return false;
29 }
30
31 struct sockaddr_in local;
32 //bzero(&local, sizeof(local));
33 memset(&local, 0, sizeof(local));
34 local.sin_family = AF_INET;
35 local.sin_port = htons(port);
36 local.sin_addr.s_addr = INADDR_ANY;
37
38 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
39 std::cerr << "bind error" << std::endl;
40 return false;
41 }
42
43 if(listen(listen_sock, BACKLOG) < 0){
44 std::cerr << "listen error" <<std::endl;
45 return false;
46 }
47 std::cout << "listen success" << std::endl;
48 return true;
49 }
50 void Loop()
51 {
52 //这样写父进程就不用等子进程了,很推荐,因为很简单
53 //signal(SIGCHLD, SIG_IGN);
54 for(;;)
55 {
56 //先获取链接
57 struct sockaddr_in peer;
58 socklen_t len = sizeof(peer);
59 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
60 //这里是accept失败
61 if(sock < 0){
62 std::cerr << "accept error" << std::endl;
63 continue;
64 }
65
66
67 pid_t id = fork();
68 if(id == 0){
69 //创建出来孙子进程是为了土共服务的
70 //所以不用关心 listen_sock了
71 close(listen_sock);//这里不是必要的
72 if(fork() > 0){
73 exit(0);
74 }
75 //孙子进程
76 //这样做的原因是父进程退出,子进程被系统领养成为孤儿进程
77 std::string ip = inet_ntoa(peer.sin_addr);
78 int port = ntohs(peer.sin_port);
79 std::cout << "get a new link:"<< sock <<"[" << ip << "]" << port << std::endl;
80 Service(sock, ip, port);//处理
81 exit(0);
82 }
83
84 //这里关掉sock不会影响孙子进程的使用
85 //因为就相当于爷爷进程将指向sock这个文件描述符关掉
86 close(sock);//这里是必要的
87 //这里还是要等的,因为父进程退出,爷爷进程是要等父进程的
88 //但是这里就不算是挂起了,因为父进程是直接就退出,爷爷进程直接回收
89 waitpid(id, nullptr, 0);//这里的id就是父进程的id,当然这里也可以写成-1
90 }
91
92 }
93
94 void Service(int sock, std::string& ip, short port)
95 {
96 char buffer[1024];
97 while(true){
98 ssize_t size = read(sock, buffer, sizeof(buffer)-1);
99 if(size > 0){
100 buffer[size] = 0;
101 std::cout << ip << ":" << port << "# " << buffer << std::endl;
102 //回显
103 write(sock, buffer, size);
104 }
105 else if (size == 0){
106 std::cout << ip << ":" << port << "close!" << buffer << std::endl;
107 break;
108 }
109 else{
110 std::cerr << port << "read error" << std::endl;
111 break;
112 }
113 }
114
115 close(sock);
116 std::cout << "Service done!" << std::endl;
117 }
118
119 ~TcpServer()
120 {
121 if(listen_sock >= 0){
122 close(listen_sock);
123 }
124 }
125 };
tcp_server.cc
1 #include "tcp_server.hpp"
2
3 int main(int argc, char* argv[])
4 {
5 if(argc != 2){
6 std::cout << "Usage: " << argv[0] << "port" << std::endl;
7 exit(1);
8 }
9 TcpServer tsvr(atoi(argv[1]));
10 tsvr.InitTcpServer();
11
12 tsvr.Loop();
13
14
15 return 0;
16 }
tcp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10 #include <signal.h>
11 #include <sys/wait.h>
12 #include <pthread.h>
13
14
15 #define BACKLOG 5
16
17 class Prama{
18 public:
19 int sock;
20 std::string ip;
21 short port;
22
23 Prama(int _sock, std::string _ip, short _port)
24 :sock(_sock)
25 ,ip(_ip)
26 ,port(_port)
27 {}
28 ~Prama()
29 {}
30 };
31
32
33 class TcpServer{
34 private:
35 short port;
36 int listen_sock;
37 public:
38 TcpServer(short _port)
39 :port(_port)
40 ,listen_sock(-1)
41 {}
42 bool InitTcpServer()
43 {
44 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
45 if(listen_sock < 0){
46 std::cerr << "socket error" << std::endl;
47 return false;
48 }
49
50 struct sockaddr_in local;
51 //bzero(&local, sizeof(local));
52 memset(&local, 0, sizeof(local));
53 local.sin_family = AF_INET;
54 local.sin_port = htons(port);
55 local.sin_addr.s_addr = INADDR_ANY;
56
57 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
58 std::cerr << "bind error" << std::endl;
59 return false;
60 }
61
62 if(listen(listen_sock, BACKLOG) < 0){
63 std::cerr << "listen error" <<std::endl;
64 return false;
65 }
66 std::cout << "listen success" << std::endl;
67 return true;
68 }
69
70 static void* HanderRequest(void* arg)
71 {
72 Prama* p = (Prama*)arg;
73
74 //这里要分离,不分离也不等的话,会出现像僵尸进程一样的东西
75 pthread_detach(pthread_self());
76
77 Service(p->sock, p->ip, p->port);
78
79 close(p->sock);
80 delete p;
81 return nullptr;
82 }
83 void Loop()
84 {
85 //这样写父进程就不用等子进程了,很推荐,因为很简单
86 //signal(SIGCHLD, SIG_IGN);
87 struct sockaddr_in peer;
88 for(;;)
89 {
90 //先获取链接
91 socklen_t len = sizeof(peer);
92 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
93 //这里是accept失败
94 if(sock < 0){
95 std::cerr << "accept error" << std::endl;
96 continue;
97 }
98
99 pthread_t tid;
100 std::string ip = inet_ntoa(peer.sin_addr);
101 short port = ntohs(peer.sin_port);
102 //这里因为可能会出现并发的问题,也就是sock的结果可能会出现问题
103 //所以我们这里有一种方法可以new一个p,这样p就肯定是属于特定一个线程了
104 Prama* p = new Prama(sock, ip, port);
105 pthread_create(&tid, nullptr, HanderRequest, p);
106
107
108 }
109
110 }
111
112 //这里我们将 sock ip port 都传进来了,所以这里我们可以设成static
113 //也为了让HanderRequete调用
114 static void Service(int sock, std::string& ip, short port)
115 {
116 char buffer[1024];
117 while(true){
118 ssize_t size = read(sock, buffer, sizeof(buffer)-1);
119 if(size > 0){
120 buffer[size] = 0;
121 std::cout << ip << ":" << port << "# " << buffer << std::endl;
122 //回显
123 write(sock, buffer, size);
124 }
125 else if (size == 0){
126 std::cout << ip << ":" << port << "close!" << buffer << std::endl;
127 break;
128 }
129 else{
130 std::cerr << port << "read error" << std::endl;
131 break;
132 }
133 }
134
135 std::cout << "Service done!" << std::endl;
136 }
137
138 ~TcpServer()
139 {
140 if(listen_sock >= 0){
141 close(listen_sock);
142 }
143 }
144 };
tcp_client.cc
1 #include "tcp_client.hpp"
2
3
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cout << "Usage: " << argv[0] << "ip port" << std::endl;
8 exit(1);
9 }
10
11 TcpClient tclt(argv[1], atoi(argv[2]));
12 tclt.InitTcpClient();
13
14 tclt.Loop();
15
16 return 0;
17 }
当然这里也是只对 tcp_server.hpp 做了处理,具体的情况,大家去看代码的不同吧,大致就是创建线程,然后让线程分离再让线程执行服务的操作,然后其中的服务也改成了 static,为了让 static 的 Hander 可以调用,又因为 Hander 里要用到 sock、ip、port ,所以设计了一个类进行保存,然后传给 Hander。
我们可以看到,和多进程相比,除了细节上的不同,效果上大致是相同的,那么这里我也就不过多赘述了。
4.5 TCP 多线程(线程池版本)
上面多线程是到用的时候,去创建新线程,没有什么问题,就是效率很低。
好的做法是,已经都有线程了,有人需要服务,那么直接使用线程就好。
还有一点是,如果线程多了,线程之间的相互切换的频率也就会高,也就是成本很高,导致效率非常低。比方说第一个线程执行了一点被切走了,直到执行到了第一千个线程才切换回来,因为线程代表的是用户,在用户看来就是你的服务提供的有问题。
所以我们这里应该设置好线程的个数,如果申请的人多,那么就让用户排队,最起码让正在接收服务的人得到好的服务。
所以这个方案就是基于多线程的线程池方案。
ThreadPool.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <queue>
5 #include <pthread.h>
6
7 #define NUM 5
8
9 template<typename T>
10 class ThreadPool{
11 private:
12 int thread_num;
13 std::queue<T> task_queue;
14 pthread_mutex_t lock;
15 pthread_cond_t cond;
16
17 public:
18 ThreadPool(int _num = NUM)
19 :thread_num(_num)
20 {
21 pthread_mutex_init(&lock, nullptr);
22 pthread_cond_init(&cond, nullptr);
23 }
24
25 //因为static不能直接访问private
26 //所以要对其进行封装
27 void LockQueue()
28 {
29 pthread_mutex_lock(&lock);
30 }
31 void UnlockQueue()
32 {
33 pthread_mutex_unlock(&lock);
34 }
35 bool IsEmpty()
36 {
37 return task_queue.empty();
38 }
39 void Wait()
40 {
41 pthread_cond_wait(&cond, &lock);
42 }
43 void Wakeup()
44 {
45 pthread_cond_signal(&cond);
46 }
47 // 这里要用static是因为在类里写成员函数时会带上
48 // this指针,但是这个参数只能传一个void*类型的参数
49 static void* Routine(void* arg)
50 {
51 pthread_detach(pthread_self());
52 ThreadPool* self = (ThreadPool*)arg;
53 while(true){
54 self->LockQueue();
55 while(self->IsEmpty()){
56 //wait
57 self->Wait();
58 }
59 //任务
60 T t;
61 self->Pop(t);
62 self->UnlockQueue();
63 //因为执行任务的时候,这个任务属于你当前线程
64 //所以接下来只需要在Unlock之后处理就可以了
65 //如果在里面处理,那其它线程要等你处理完才可以处理下一个
66 //如果这样,虽然是线程池,那也没有让多个线程同时跑起来
67 t.Run();
68 }
69 }
70 void InitThreadPool()
71 {
72 pthread_t tid;
73 for(int i = 0; i < thread_num; i++){
74 pthread_create(&tid, nullptr, Routine, this);//所以这里要传this
75 }
76 }
77
78 void Push(const T& in)
79 {
80 LockQueue();
81 task_queue.push(in);
82 UnlockQueue();
83 //插入数据后其它线程的不知道
84 //所以要唤醒其它线程
85 Wakeup();
86 }
87
88 void Pop(T& out)
89 {
90 out = task_queue.front();
91 task_queue.pop();
92 }
93
94 ~ThreadPool()
95 {
96 pthread_mutex_destroy(&lock);
97 pthread_cond_destroy(&cond);
98 }
99 };
Task.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <unistd.h>
6
7 class Handler{
8 public:
9 Handler(){}
10
11 void operator()(int sock, std::string ip, short port)
12 {
13 char buffer[1024];
14 while(true){
15 ssize_t size = read(sock, buffer, sizeof(buffer)-1);
16 if(size > 0){
17 buffer[size] = 0;
18 std::cout << ip << ":" << port << "# " << buffer << std::endl;
19 //回显
20 write(sock, buffer, size);
21 }
22 else if (size == 0){
23 std::cout << ip << ":" << port << "close!" << buffer << std::endl;
24 break;
25 }
26 else{
27 std::cerr << port << "read error" << std::endl;
28 break;
29 }
30 }
31
32 std::cout << "Service done!" << std::endl;
33 close(sock);
34 }
35
36 ~Handler(){}
37 };
38
39
40 class Task{
41 private:
42 int sock;
43 std::string ip;
44 short port;
45 Handler handler;
46 public:
47 Task(){}
48 Task(int _sock, std::string _ip, short _port)
49 :sock(_sock)
50 ,ip(_ip)
51 ,port(_port)
52 {}
53
54
55 void Run()
56 {
57 handler(sock, ip, port);
58 }
59
60 ~Task()
61 {}
62 };
tcp_server.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10 #include <signal.h>
11 #include <sys/wait.h>
12 #include <pthread.h>
13 #include "ThreadPool.hpp"
14 #include "Task.hpp"
15
16 #define BACKLOG 5
17
18 class Prama{
19 public:
20 int sock;
21 std::string ip;
22 short port;
23
24 Prama(int _sock, std::string _ip, short _port)
25 :sock(_sock)
26 ,ip(_ip)
27 ,port(_port)
28 {}
29 ~Prama()
30 {}
31 };
32
33
34 class TcpServer{
35 private:
36 short port;
37 int listen_sock;
38 ThreadPool<Task>* tp;
39 public:
40 TcpServer(short _port)
41 :port(_port)
42 ,listen_sock(-1)
43 ,tp(nullptr)
44 {}
45 bool InitTcpServer()
46 {
47 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
48 if(listen_sock < 0){
49 std::cerr << "socket error" << std::endl;
50 return false;
51 }
52
53 struct sockaddr_in local;
54 //bzero(&local, sizeof(local));
55 memset(&local, 0, sizeof(local));
56 local.sin_family = AF_INET;
57 local.sin_port = htons(port);
58 local.sin_addr.s_addr = INADDR_ANY;
59
60 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
61 std::cerr << "bind error" << std::endl;
62 return false;
63 }
64
65 if(listen(listen_sock, BACKLOG) < 0){
66 std::cerr << "listen error" <<std::endl;
67 return false;
68 }
69
70 tp = new ThreadPool<Task>();
71
72 std::cout << "listen success" << std::endl;
73 return true;
74 }
75
76 void Loop()
77 {
78 //这样写父进程就不用等子进程了,很推荐,因为很简单
79 //signal(SIGCHLD, SIG_IGN);
80 tp->InitThreadPool();
81 struct sockaddr_in peer;
82 for(;;)
83 {
84 //先获取链接
85 socklen_t len = sizeof(peer);
86 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
87 //这里是accept失败
88 if(sock < 0){
89 std::cerr << "accept error" << std::endl;
90 continue;
91 }
92
93 std::string ip = inet_ntoa(peer.sin_addr);
94 short port = ntohs(peer.sin_port);
95
96 Task t(sock, ip, port);
97
98 tp->Push(t);
99
100 }
101 }
102
103 ~TcpServer()
104 {
105 if(listen_sock >= 0){
106 close(listen_sock);
107 }
108 }
109 };
tcp_server.cc
1 #include "tcp_server.hpp"
2
3 int main(int argc, char* argv[])
4 {
5 if(argc != 2){
6 std::cout << "Usage: " << argv[0] << "port" << std::endl;
7 exit(1);
8 }
9 TcpServer tsvr(atoi(argv[1]));
10 tsvr.InitTcpServer();
11
12 tsvr.Loop();
13
14
15 return 0;
16 }
tcp_client.hpp
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <cstring>
6 #include <sys/socket.h>
7 #include <sys/types.h>
8 #include <arpa/inet.h>
9 #include <unistd.h>
10 #include <signal.h>
11 #include <sys/wait.h>
12 #include <pthread.h>
13
14
15 #define BACKLOG 5
16
17 class Prama{
18 public:
19 int sock;
20 std::string ip;
21 short port;
22
23 Prama(int _sock, std::string _ip, short _port)
24 :sock(_sock)
25 ,ip(_ip)
26 ,port(_port)
27 {}
28 ~Prama()
29 {}
30 };
31
32
33 class TcpServer{
34 private:
35 short port;
36 int listen_sock;
37 public:
38 TcpServer(short _port)
39 :port(_port)
40 ,listen_sock(-1)
41 {}
42 bool InitTcpServer()
43 {
44 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
45 if(listen_sock < 0){
46 std::cerr << "socket error" << std::endl;
47 return false;
48 }
49
50 struct sockaddr_in local;
51 //bzero(&local, sizeof(local));
52 memset(&local, 0, sizeof(local));
53 local.sin_family = AF_INET;
54 local.sin_port = htons(port);
55 local.sin_addr.s_addr = INADDR_ANY;
56
57 if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
58 std::cerr << "bind error" << std::endl;
59 return false;
60 }
61
62 if(listen(listen_sock, BACKLOG) < 0){
63 std::cerr << "listen error" <<std::endl;
64 return false;
65 }
66 std::cout << "listen success" << std::endl;
67 return true;
68 }
69
70 static void* HanderRequest(void* arg)
71 {
72 Prama* p = (Prama*)arg;
73
74 //这里要分离,不分离也不等的话,会出现像僵尸进程一样的东西
75 pthread_detach(pthread_self());
76
77 Service(p->sock, p->ip, p->port);
78
79 close(p->sock);
80 delete p;
81 return nullptr;
82 }
83 void Loop()
84 {
85 //这样写父进程就不用等子进程了,很推荐,因为很简单
86 //signal(SIGCHLD, SIG_IGN);
87 struct sockaddr_in peer;
88 for(;;)
89 {
90 //先获取链接
91 socklen_t len = sizeof(peer);
92 int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
93 //这里是accept失败
94 if(sock < 0){
95 std::cerr << "accept error" << std::endl;
96 continue;
97 }
98
99 pthread_t tid;
100 std::string ip = inet_ntoa(peer.sin_addr);
101 short port = ntohs(peer.sin_port);
102 //这里因为可能会出现并发的问题,也就是sock的结果可能会出现问题
103 //所以我们这里有一种方法可以new一个p,这样p就肯定是属于特定一个线程了
104 Prama* p = new Prama(sock, ip, port);
105 pthread_create(&tid, nullptr, HanderRequest, p);
106
107
108 }
109
110 }
111
112 //这里我们将 sock ip port 都传进来了,所以这里我们可以设成static
113 //也为了让HanderRequete调用
114 static void Service(int sock, std::string& ip, short port)
115 {
116 char buffer[1024];
117 while(true){
118 ssize_t size = read(sock, buffer, sizeof(buffer)-1);
119 if(size > 0){
120 buffer[size] = 0;
121 std::cout << ip << ":" << port << "# " << buffer << std::endl;
122 //回显
123 write(sock, buffer, size);
124 }
125 else if (size == 0){
126 std::cout << ip << ":" << port << "close!" << buffer << std::endl;
127 break;
128 }
129 else{
130 std::cerr << port << "read error" << std::endl;
131 break;
132 }
133 }
134
135 std::cout << "Service done!" << std::endl;
136 }
137
138 ~TcpServer()
139 {
140 if(listen_sock >= 0){
141 close(listen_sock);
142 }
143 }
144 };
tcp_client.cc
1 #include "tcp_client.hpp"
2
3
4 int main(int argc, char* argv[])
5 {
6 if(argc != 3){
7 std::cout << "Usage: " << argv[0] << "ip port" << std::endl;
8 exit(1);
9 }
10
11 TcpClient tclt(argv[1], atoi(argv[2]));
12 tclt.InitTcpClient();
13
14 tclt.Loop();
15
16 return 0;
17 }
当然这里就有很多的细节了,我先来说整体的思路,然后具体的实现大家去对照上面的代码去看看:
我先说下,线程池这块我因为在多线程的时候讲过,所以这里我就不讲了,大家可以去看我多线程的博客,写的很详细,从零到尾讲的很详细,链接也给大家贴到最开始了。
这里就是在服务器中先初始化线程池,然后将服务器的循环里改为了线程池,然后主线程就一直将任务 Task 放到任务队列里。
因为在 tcp_server.hpp 中要将任务放到任务队列里,所以就将原本 tcp_server.hpp 里的让服务器和用户通信的过程放到了 Task.hpp 里,在 Task 中通过 Run 调用 handler 方法,其中应用了回调函数,这样也相当于进行了解耦,如果想更改任务直接更改 handler 方法中的方式就可以了。
这就是大体的思路,具体的细节大家去看看代码,自己动手去写写吧,接下来给大家展示效果。
我们可以看到,从开始到最后,不管有几个客户端向服务器端申请,都只有特定的线程数个,当然这里我默认设置成的 5 个,这个可以自己设置。
5. 零碎知识点
5.1 地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换。
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
代码示例:
5.2 关于inet_ntoa
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?
man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。
那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?
参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
- 在APUE中,明确提出inet_ntoa不是线程安全的函数。
- 但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁。
- 在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
如上就是 网络编程套接字 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!
再次感谢大家观看,感谢大家支持!