文章目录
- 与UDP_SOCKET的区别
- 第一代Tcp_Server
- Tcp_Client
- 第二代Tcp_Server
- 第三代Tcp_server
- 多线程版本Tcp_Server
- 线程池版的Tcp_Server
- 使用inet_ntop来解决线程安全问题
- 业务逻辑编写
- 总结
- 补充说明&&业务代码完成
- ping的真实作用
- Translate编写
- Transform业务代码
- 整体总结
与UDP_SOCKET的区别
与udp大同小异
多了一些步骤
udp是不可靠的, 不连接的协议
tcp是面向连接的
c/s
谁来建立这个链接?
是client主动建立链接
如手机上的抖音打开, 淘宝打开等
服务器是在一直等待链接的到来, 好比餐厅老板等待客人到来
所以与udp的区别
- 设置socket为监听状态
函数接口:
参数2后续解释
第一代Tcp_Server
tcp初始化操作, 都是固定套路
初始化代码实现:
void Init(){// 1. 创建socket, file fd, 本质是文件_sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建tcp套接字, 第一个参数不变, 第二个参数是tcp, 面向字节流if (_sockfd < 0){lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal);}lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _sockfd);// 2. 填充网络信息并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_addr.s_addr = htonl(INADDR_ANY);//必须是0, 表示任意local.sin_family = AF_INET;//IPv4的网络套接字local.sin_port = htons(_port);local.sin_zero[0] = 0;//填充剩余部分, 必须置零if(bind(_sockfd, CONV(&local), sizeof(local) != 0)){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _sockfd);// 3.设置socket为监听状态if(listen(_sockfd, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _sockfd);
- tcp_server获取链接
后两个参数是输入输出型, 更强调输出,
这个参数相当于udp的recvfrom
他的返回值成功会返回一个非0的文件描述符
udp里面, sockfd只有一个
-
但是在tcp这里, 会新增一个文件描述符
eg: 门口拉客的 = 旧的sockfd
店内传菜的 = 新创建的sockfd
使用旧的sockfd和client进行获取链接
使用新的sockfd和client进行通信
这样看来, 旧的sockfd只用于listen
新的sockfd才是真的sockfd -
udp是面向数据报, 而tcp是面向字节流, 这和文件, 管道的特性一模一样
Start启动完成
void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);// 进行获取链接if(sockfd < 0){lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;// 获取失败则继续获取}lg.LogMessage(Debug, "accept socket success, get a new sockfd: %d\n", sockfd);// 5. 提供服务Service(sockfd);close(sockfd);} }// 这个sockfd表示连接是全双工的void Service(int sockfd){char buffer[1024];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0)// read如果为0, 文件中表示读到了结尾, 这边表示对端关闭了连接(与管道的情况一模一样){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}
- 查看结果
tcp_client编写
与udp的差别, tcp在创建了sockfd之后, 只需要建立连接
Tcp_Client
与udp的差别, tcp在创建了sockfd之后, 只需要建立连接
-
connect连接
-
文件流进行消息读写
// 2. client必须要ip和port, 但是不需要显示绑定, client 系统随机端口// tcp发起链接的时候, 被OS自动进行本地绑定// 建立连接connectstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// server.sin_addr.s_addr = inet_addr(serverip.c_str());// 换一个接口 ipv4转二进制 p当成process n是网络 -- 不太准确, 但是好记inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 1. IPV4--> 四字节ip 2. 进程转网络序列int n = connect(sockfd, CONV(&server), sizeof(server)); // 这里自动进行bindif (n < 0){cerr << "connect error" << endl;return 2;}// 3. 发送数据// 与服务器进行持续通信的循环while (true){// 用户输入的消息string message;// 提示用户输入信息cout << "Please Enter# ";// 读取用户输入的整行文本getline(cin, message);// 尝试向服务器发送消息ssize_t n = write(sockfd, message.c_str(), message.size());// 检查消息是否成功发送if (n > 0){// 用于接收服务器响应的缓冲区char buffer[1024];// 从服务器读取响应, sockfd读写都用, 更加说明是全双工ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);// 检查是否接收到响应if (m > 0){// 确保响应字符串以空字符结尾buffer[m] = 0;// 打印服务器的响应std::cout << "get a echo message# " << buffer << std::endl;}// 检查服务器是否关闭连接else if (m == 0){// 服务器关闭时打印信息并退出循环std::cout << "server close!" << std::endl;break;}}// 发送消息时发生错误else{// 打印错误信息并退出循环cerr << "write error" << endl;break;}}close(sockfd);
进行测试:
结果说明
1. 运行后查看netstat -ntp 有两个服务, 不是一个吗?
原因是, 现在client和server在一台机器上, 所有有两个, 将来如果是在一台机器上, 那就只有一个
2. 这样的新创建的client因为是单进程, 所以新建的client 会阻塞, 发的消息也会阻塞, 当旧的client关闭, 新的client会瞬间显示阻塞的信息, 这个阻塞的极限是多少, 由我们服务器的缓冲区决定
可以看到, 消息被输出后, 一部分成功输出, 另一部分write失败, 导致溢出到shell中输出
3. 服务器断线重连实现:
a. 将上述的工作放到visitServer函数中
然后, 进行断线重连的操作
int count = 1;while(count <= ReTry_count){bool result = visitServer(serverip, serverport);if(result) break;else{// 重连操作sleep(1);cout << "reconnecting..., count: " << count++ << endl;}}if(count > ReTry_count) {cout << "reconnect failed" << endl;}
b. 有时client端会在创建后, server退出, 此时client重连, 让他实现从1次重连开始
像这样,就需要修改代码
只需让他多加一个参数, 在这个函数内返回false时, 让count = 1即可, 这个意义不太大, 有了解即可, 因为现实情况是远比当前复杂, 有可能是客户端断线…等等
- tcp_server启动有时候会出现绑定失败的情况
原理先不解释, 先说解决方式
setsockopt
写在server创建listensockfd那里
第二代Tcp_Server
多进程版本的相互通信
对于一个父进程, 他有他自己的文件描述符表, 且表中的每个信息指向它对应的struct file, 创建子进程之后, 会有多个表中的信息指向同一个struct file, 所以可以像管道那样, 对于父子进程关闭不需要的文件fd
第三代Tcp_server
信号版本的Tcp_server
在linux 中, 如果对SIGCHLD设置为SIG_IGN, 则会自动回收子进程, 不会wait阻塞住
所以开局设置signal
然后创建子进程方面默认执行
因为设置了信号机制, 所以也不用wait
子进程的退出会自动OS回收, 如果不设置信号, 单纯使用上述的代码, 会引起僵尸进程问题
每次创建进程都会消耗时间, 推荐使用进程池来使用, 这边就不进行讲述
问题:
先创建的子进程, 再进行的链接, 进程看不到sockfd,怎么办?
在父子进程之间传递文件描述符的实现, 一个比较老的功能, 可以通过这个解决问题
多线程版本Tcp_Server
相比多进程, 更加的方便, 也不需要文件描述符的传递, 也不需要关闭所谓的fd, 主子线程共享文件描述符表
完成之后, 为什么有报错?
主要是因为类型不匹配导致的
可以考虑使用std::bind来适配成员函数的调用格式,但是更推荐重构成员函数为静态函数或全局函数
传参
定义实现
封装
结果演示
每加一个线程, 文件描述符表也会增加
链接来了, 才创建线程, 还是慢, 所以使用线程池, 且加上client的信息, 通过accept函数获取, 后两个参数也有输出功能
线程池版的Tcp_Server
先加client信息的处理
使用InetAddr.hpp获取属性
相关代码改动
完成之后, 进行验证
相关问题
使用inet_ntop来解决线程安全问题
这个转IP的函数返回值是一个char*, 未来的操作可能会有线程安全问题
他会在函数内部申请一个静态的内存地址, 然后返回, 所以他是一个可重入函数, 存在线程安全问题
应当减少使用, 推荐使用inet_ntop, 这个函数允许我们自己维护一个缓冲区, 而不是函数内部自己去申请
可以有效避免线程安全问题
多线程修改:
防止命名冲突, 将threadpool放在ThreadNs里面, 然后再TcpServer的构造函数里面
下面进行修改代码, 让server能被所有线程就进行访问
往后的任务都是线程池中的线程来接受任务, 进行处理, 而不是创建新的线程
下面实现代码让线程来执行任务
这个任务可以使用Task类来进行处理, 但是也可以使用function来进行处理
这样的方式比较简单
下面用这个包装器来完成任务
未来获取新的文件文件描述符信息等后, 可以直接进行如下构建
再这样修改完成之后, 因为线程池中的线程数目是固定的, 所以我们的client链接数目有限, 当超过限制的client数目时, 就会出现, 连不上的情况. 同时, 为别人提供的服务是死循环式对我们的server端的压力是有点大的, 所以在一般我们很少去这样写代码
现在对他进行调整, 让它变得实际有用一点, 而不是像现在这样进行通信
也就是说, 不能让我们的服务像是死循环这样一直执行, 而是实现一个业务逻辑
这边的策略是实现一个英译汉的功能,要利用I/O, 同时引入unordered_map
定义回调函数(后期可能进行修改)
现在只需要Tcp_server进行IO就可以了
然后构建业务逻辑
业务逻辑编写
在这里存放所有的任务
接下来给Tcp_Server一个register功能
Read方式是进行读取用户第一次输入必须是输入他想要的功能
在 ping Translate Transform之间三选一
read看是读取了哪个结果
进行一个Routine 的路由功能, 实现read到对应的内容给type, 好实现对应的功能, 对应的功能是放在funcs中的
然后, 线程在启动执行的时候, 将对应的sockfd 的功能和Routine进行绑定
然后, 服务器创建出来之后(这一步在Main.cc文件中), 执行注册服务
注册之后, 执行对应的服务, 以便调用时执行对应的回调函数
这个register进行添加对应的name和方法到map
然后在进行Start的时候, 将要执行的方法和对应的用户输入名称之间的匹配调用, 此时Ping之类的函数才算是被真正的执行
Ping函数要做的事就是之前在Tcp_server里面, server方法要做的事
然后在修改Tcp_Client
完成代码进行执行查看, 可以正常执行
server-log
client-log
现在补充其他的路由信息,进行基本的完善
具体功能后续进行补齐
现在ping的作用事是想实现一下一次ping之后的结果, 所以还要进行修改代码
然后因为之前是将Sockfd的关闭放在tcp_server的ThreadData类的析构函数中的, 但是现在已经不需要这个类了, 所以还要进行文件描述符的关闭操作, 这个操作可以放在这个回调函数执行完毕的后面
下面继续执行代码
可以看到在一次运行结束之后, 这个client会进行退出, 主要是因为当期的ping服务已经变成了短服务,只需要执行一次, 而不是像之前那样的死循环
现在进行总结一下
总结
未来服务可能部署在云服务器上面, 如何在未来的某一时刻知道这个服务器是否健康的呢?
可以定期(30s)给服务器发送一个最小服务请求, 如果得到回复, 那么这个服务就是正常的
这个机制, 我们称为心跳机制, 我们可以通过client->服务器, 同时也可以反向的得到server->client
而这个编写的Ping, 其实就是对心跳的一个响应机制, 用于检测服务器是否是正常的
补充说明&&业务代码完成
ping的真实作用
Interact是进行交互功能, 它可以进行心跳机制的检测, 读消息是out, 发消息是in, 以此完成交互
下面进行Translate
Translate编写
首先, 我们到此为止,所有的代码都是网络
下面是业务代码, 顺便完成Translate
为了方便, 可以将单词加载在resource的dict.txt文件内, 进行文件操作
然后构建Translate.hpp业务逻辑代码
定义正常的常变量
字典路径, 当前字典, 当前字典读取的内容
然后是构造函数和加载字典的方法
然后是内部函数
现在可以DEBUG一下, 看看有没有读取成功
推荐使用条件编译
但是这个是.hpp文件, 所以不能这样进行条件编译操作
可以创建一个test.cc来进行debug
说明我们在Translate的构造函数, 读取字典函数, private变量都没有问题
然后进行分割功能
做完分割, 那么可以对这个Debug进行修改
未来的加载词库, 只需要往dict.txt中加就行了. 当然更推荐将这个Translate类改为单例模式, 这边就不做处理
那么如何将这个Translate服务于网络进行结合呢?
那就到了Translate调用
首先定义全局变量(改为单例最好)
所以未来服务首先都是进行注册的
Transform业务代码
将1按照OP的格式转化为result存到1里面
代码完成, 对于defaultserver的部分修改就不演示
整体总结
- IO类函数, write/read其实他们已经做了转网络序列的处理
- udp :数据包 tcp: 面向字节流
a. 区别:
1). TCp代码中我们使用read/write目前是有BUG的(下节讲解), TCP要进行正确读写, 必须结合用户协议
2). udp-----数据和数据是有边界的, sendto只发了一次, 对端对应对端, recvfrom一次----面向数据报
3). tcp------write->1,10, 100->接收方可能一次收完, 也可能是很多次, 但无论是多少次,都和对方发送无关—面向字节流
b.udp就好比是一封信件, 只能一封一封去读
c. tcp是全读, 然后进行手动分割 - 网络服务在真正运行时, 必须在Linux中以守护进程(精灵进程)的形式进行运行
要完成上述工作, 应当利用守护进程, 下篇讲解~~~~~