当前位置: 首页 > news >正文

【网络编程】揭开套接字的神秘面纱

文章目录

  • 1 :peach:简单理解TCP/UDP协议 :peach:
  • 2 :peach:网络字节序 :peach:
  • 3 :peach:socket编程接口 :peach:
    • 3.1 :apple:socket 常见API :apple:
    • 3.2 :apple:sockaddr结构:apple:
  • 4 :peach:简单的UDP网络程序 :peach:
    • 4.1 :apple:基本分析:apple:
    • 4.2 :apple:udpServer.hpp(重点):apple:
      • 4.2.1 :lemon:注意事项:lemon:
    • 4.3 :apple:udpClient.cc:apple:
      • 4.3.1 :lemon:注意事项:lemon:
    • 4.4 :apple:udpServer.c:apple:
    • 4.5 :apple:如何关闭防火墙+验证:apple:
  • 5 :peach:简单的TCP网络程序 :peach:
    • 5.1 :apple:tcpServer.hpp(重要):apple:
      • 5.1.1 :lemon:注意事项:lemon:
    • 5.2 :apple:tcpClient.cc:apple:
      • 5.2.1 :lemon:注意事项:lemon:
    • 5.3 :apple:tcpServer.cc:apple:
    • 5.4 :apple:验证:apple:
      • 5.4.1 :lemon:多进程:lemon:
      • 5.4.2 :lemon:多线程:lemon:
  • 6 :peach:TCP协议通讯流程:peach:


1 🍑简单理解TCP/UDP协议 🍑

TCP协议:

  • 1️⃣传输层协议
  • 2️⃣有连接
  • 3️⃣可靠传输
  • 4️⃣面向字节流

UDP协议:

  • 1️⃣传输层协议
  • 2️⃣无连接
  • 3️⃣不可靠传输
  • 4️⃣面向数据报

2 🍑网络字节序 🍑

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可.

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

       #include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);

3 🍑socket编程接口 🍑

3.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);

3.2 🍎sockaddr结构🍎

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
在这里插入图片描述
所以当我们使用的时候可以将地址强转成 sockaddr* 类型。

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr *; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

struct sockaddr的定义:
在这里插入图片描述struct sockaddr_in的定义:
在这里插入图片描述


4 🍑简单的UDP网络程序 🍑

4.1 🍎基本分析🍎

在写之前,我们先来简单的分析分析下我们应该怎样写?首先我们封装一个udpServer的类来帮助我们创建套接字以及套接字的初始换工作,当然客户端也可以使用这种方式来完,不过由于客户端的代码很简单,我就不在封装一个udpClient的类了。
其次我们思考下udpServer类中成员应该有哪些?
首先肯定要一个套接字(其本质就是一个文件描述符),其次我们需要一个端口号,大家猜一下,我们需要一个IP地址吗?这个其实是不需要的,因为一款服务器/云服务器一般是不要指定某一个具体的IP地址的.
那我们bind的时候应该怎样传入参数呢?这个大家先不急,等会儿将代码写好了大家在回过来看就会清晰很多。为了方便使用我们还可以用一个包装器来包装我们将来要执行回调的函数。

4.2 🍎udpServer.hpp(重点)🍎

#pragma once 
#include<iostream>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include<functional>
using namespace std;using fun_t =function<string(string)>;
class udpServer
{
public:const static uint16_t defaultPort=8848;udpServer(fun_t service=nullptr, uint16_t port =defaultPort):_service(service),_port(port){}void init(){//1 创建套接字,打开网络文件_socket=socket(AF_INET,SOCK_DGRAM,0);if(_socket<0){cerr<<"create socket fail"<<endl;exit(-1);}//2 bindsockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;if(bind(_socket,(sockaddr*)&local,sizeof(local))<0){cerr<<"bind fail"<<endl;exit(-2);}cout<<"bind success"<<endl;}void start(){char buffer[1024];//自定义缓冲区while(true){//1 从客户端收消息sockaddr_in client;//用作输出型参数,用来接受是哪个具体的客户端发送数据给服务端的socklen_t len=sizeof(client);int n=recvfrom(_socket,buffer,sizeof(buffer)-1,0,(sockaddr*)&client,&len);if(n>0)buffer[n]=0;elsecontinue;// cout<<"receive message success"<<endl;string clientIp=inet_ntoa(client.sin_addr);uint16_t clientPort=ntohs(client.sin_port);cout<<clientIp<<"-"<<clientPort<<":"<<buffer<<endl;//2 处理消息string message=_service(buffer);//3 发送消息给客户端if(sendto(_socket,message.c_str(),message.size(),0,(sockaddr*)&client,sizeof(client))<0){cerr<<"send message fail"<<endl;exit(-3);}//cout<<"send message success"<<endl;}}
private:int _socket;uint32_t _port;fun_t _service;
};

4.2.1 🍋注意事项🍋

  • 1️⃣ 创建套接字所要的头文件是:
 #include <sys/types.h>          /* See NOTES */#include <sys/socket.h>

但是sockaddr_in是定义在下面的头文件中的:

#include <netinet/in.h>
#include <arpa/inet.h>

所以我们写套接字编程的时候,这四个头文件都要带上。

  • 2️⃣由于我们使用的是udp协议,所以我们使用的是SOCK_DGRAM,如果是tcp协议,我们使用的是SOCK_STREAM
    在这里插入图片描述
    至于第三个参数默认给0即可。

  • 3️⃣在bind的时候我们由于类中成员并没有加上IP地址,所以我们使用下面这种写法:
    在这里插入图片描述

4.3 🍎udpClient.cc🍎

#include"udpServer.hpp"//./udpClient serverIp serverPortvoid usage()
{cout<<"Usage error\n\t"<<"serverIp serverPort"<<endl;exit(-1);
}int main(int argc,char*args [])
{if(argc!=3){usage();}string serverIp=args[1];uint16_t serverPort=stoi(args[2]);//1 创建套接字int sock=socket(AF_INET,SOCK_DGRAM,0);if(sock<0){cout<<"create socket fail"<<endl;exit(-1);}//2 client要不要bind呢?要不要自己bind呢?//要bind 但是不要自己bind 操作系统会帮助我们做这件事情// 2 明确serversockaddr_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());while(true){//1 用户输入string message;cout<<"[grm]:";getline(cin,message);sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));//2 接受服务端信息char buffer[1024];sockaddr_in tmp;socklen_t len=sizeof(tmp);int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(sockaddr*)&tmp,&len);if(n>0){buffer[n]=0;cout<<buffer<<endl;}}return 0;
}

4.3.1 🍋注意事项🍋

  • 1️⃣在客户端这里,我们不难发现我们是没有自己手动bind的,为什么呢?
    在这之前我们先要明确一点,就是客户端也是必须要bind的,这件事只不过是操作系统帮助我们做了。但是大家肯定又有一个疑问:为什么服务端我们要自己手动bind呀?
    server的端口号要我们自己bind是因为服务器的端口号是众所周知的,且不能够随意改变;客户端不需要我们手动bind是因为害怕我们自己bind端口号时会发生冲突,所以这件事就交给了操作系统来帮助我们做。

  • 2️⃣在明确服务端的时候我们使用了下面的接口函数:
    在这里插入图片描述

这个函数有两个作用:

  1. 将字符串类型转化成四字节的uint32_t类型的四字节整数;
  2. 将主机序列转化成网络序列。

与这个函数具有同种功能的函数还有inet_aton
在这里插入图片描述
而上面的inet_ntoa则是与inet_aton具有相反的功能。
除此之外,还有inet_ptoninet_ntop:
在这里插入图片描述在这里插入图片描述在这个系列的转换函数中不仅可以转换IPV4的地址,也可以转换IPV6的地址。

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
在这里插入图片描述man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
在这里插入图片描述因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?在APUE中, 明确提出inet_ntoa不是线程安全的函数;但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

4.4 🍎udpServer.c🍎

#include<memory>
#include"udpServer.hpp"string dealMessage(const string& message)
{return message;
}void usage()
{cout<<"Usage error\n\t"<<"serverPort"<<endl;exit(0);
}//./udpServer serverPort
int main(int argc,char* argv[])
{if(argc!=2){usage();}unique_ptr<udpServer> udpSer(new udpServer(dealMessage,8848));udpSer->init();udpSer->start();return 0;
}

上述准备工作做好了后就可以来上手验证了:
注意我们在运行客户端的可执行程序时加上的IP地址可以直接是127.0.0.1(表示本机),如果想要其他主机也能够正确访问的话要加上服务端的IP,也就是我们购买云服务器的公网IP地址。

4.5 🍎如何关闭防火墙+验证🍎

如果使用了云服务器的公网IP地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:
在这里插入图片描述最后点击确认,就可以了,我们就发现列表中多出了两条:
在这里插入图片描述
到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:

在这里插入图片描述这样我们就完成了一个简易版本的UDP网络通信的代码了。

除此之外,我们还可以实现一个客户端把命令给服务端,然后服务端在帮助我们执行:

static bool isPass(const std::string &command)
{   bool pass = true;auto pos = command.find("rm");if(pos != std::string::npos) pass=false;pos = command.find("mv");if(pos != std::string::npos) pass=false;pos = command.find("while");if(pos != std::string::npos) pass=false;pos = command.find("kill");if(pos != std::string::npos) pass=false;return pass;
}// 让客户端本地把命令给服务端,server再把结果给你!
// ls -a -l
std::string excuteCommand(std::string command) // command就是一个命名
{// 1. 安全检查if(!isPass(command)) return "you are bad man!";// 2. 业务逻辑处理FILE *fp = popen(command.c_str(), "r");if(fp == nullptr) return "None";// 3. 获取结果了char line[1024];std::string result;while(fgets(line, sizeof(line), fp) != NULL){result += line;}pclose(fp);return result;
}

当我们运行时:
在这里插入图片描述不难发现已经验证成功了。
上述代码中我们简单介绍下popen函数:
在这里插入图片描述这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)

当然在客户端和服务端中我们修改代码为生产者消费者模型(具体实现可以让一个线程读取消息,另外一个线程收消息)由于同一个文件描述符可以同时被多个线程读取,所以这样设计是OK的。这里我就不实验了,大家有兴趣可以自行下去尝试。


5 🍑简单的TCP网络程序 🍑

TCP的网络程序大致框架与UDP类似,其中不同点我会放在后面一点一点给出解释。

5.1 🍎tcpServer.hpp(重要)🍎

#pragma once
#include "err.hpp"
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include<signal.h>using namespace std;
using func_t = function<string(const string &)>;
static const int backlog = 32;class tcpServer
{
public:tcpServer(func_t func, uint16_t port): _func(func), _port(port){}void init(){// 1 创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){cerr << "creat sock fail:" << strerror(errno) << endl;exit(SOCK_ERR);}// 2 bindsockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0){cerr << "bind fail" << endl;exit(BIND_ERR);}// 3 listenif (listen(_listensock, backlog) < 0){cerr << "listen fail" << strerror(errno) << endl;exit(LISTEN_ERR);}}void service(int sock, const string &clientip, const uint16_t &clientport){string who = clientip + "-" + std::to_string(clientport) + ":";char buffer[1024];while (true){// 1 读取消息ssize_t n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;// 2 处理消息string message = _func(buffer);cout << who << message << endl;// server 发送消息给 clientint n = write(sock, message.c_str(), message.size());if (n < 0){cerr << "write fail" << strerror(errno) << endl;exit(WRITE_ERR);}}else if (n == 0){cout << "client:" << clientip << "-" << to_string(clientport) << "quit,server also quit" << endl;close(sock);}else{cerr << "read fail" << strerror(errno) << endl;exit(READ_ERR);}}}void start(){while (true){// 1 获取连接 明确是哪一个client发送来的sockaddr_in client;socklen_t len;int sock = accept(_listensock, (sockaddr *)&client, &len);if (sock < 0){cerr << "accept fail" << strerror(errno) << endl;continue;}std::string clientip = inet_ntoa(client.sin_addr);uint16_t clientport = ntohs(client.sin_port);cout << "get new link success:" << sock << " form " << _listensock << endl;// 2 处理消息service(sock, clientip, clientport);private:int _listensock;uint16_t _port;func_t _func;
};

5.1.1 🍋注意事项🍋

  • 1️⃣由于是TCP,所以我们创建套接字时必须使用SOCK_STREAM.
  • 2️⃣由于TCP是保证可靠性的面向字节流的可靠协议,所以TCP在使用上肯定会比UDP复杂得多,会多上listen(监听) 和 accept (获取连接)。在linten接口的创建中我们使用的第二个参数backlog我们将放在后面再讲解,这里不太好解释。accept接口的返回值也是一个套接字,这个套接字的任务是专门用来帮助我们读取和接受消息用的,而类中的_listensock套接字的作用主要是进行前面套接字的创建和初始化工作。(可以简单的理解为_listensock就相当于餐厅里在外面招呼客人的服务员,accept接口的返回值套接字就是为客户真正意义上做饭的厨师)
  • 3️⃣我们将处理消息封装在了一个接口service中,在里面我们可以清晰得看见,读取消息用的是read,发送消息用的是write,这正是我们学习文件操作时所用到得系统调用,这也很好的印证在LINUX下一切皆文件的思想。
  • 4️⃣ 代码中所存在的错误都用了错误码来标识,错误码可参考下面:
enum 
{SOCK_ERR=1,BIND_ERR,USAGE_ERR,LISTEN_ERR,ACCEPT_ERR,CONNECT_ERR,WRITE_ERR,READ_ERR,
};

tcpClient.cc:

#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "err.hpp"
using namespace std;static void usage(string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc,char*argv[])
{if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}// 1 创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "creat sock fail:" << strerror(errno) << endl;exit(SOCK_ERR);}//2 client要bind,但是是不需要我们自己bind的//client需要listen和accept吗?答案是不需要的//3 connectstring serverip=argv[1];uint16_t serverport=stoi(argv[2]);sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_aton(serverip.c_str(), &(server.sin_addr));int cnt = 5;while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0){sleep(1);cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;if(cnt <= 0) break;}if(cnt <= 0){cerr << "连接失败..." << endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout << "Enter>>> ";getline(cin, line);write(sock, line.c_str(), line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;cout << "server echo >>>" << buffer << endl;}else if(s == 0){cerr << "server quit" << endl;break;}else {cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

5.2 🍎tcpClient.cc🍎

#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include "err.hpp"
using namespace std;static void usage(string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc,char*argv[])
{if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}// 1 创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "creat sock fail:" << strerror(errno) << endl;exit(SOCK_ERR);}//2 client要bind,但是不需要我们自己bind的//client需要listen和accept吗?答案是不需要的//3 connectstring serverip=argv[1];uint16_t serverport=stoi(argv[2]);sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_aton(serverip.c_str(), &(server.sin_addr));int cnt = 5;while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0){sleep(1);cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;if(cnt <= 0) break;}if(cnt <= 0){cerr << "连接失败..." << endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout << "Enter>>> ";getline(cin, line);write(sock, line.c_str(), line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;cout << "server echo >>>" << buffer << endl;}else if(s == 0){cerr << "server quit" << endl;break;}else {cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

5.2.1 🍋注意事项🍋

  • 1️⃣与UDP类似在bind的时候需要bind,但是这个工作不由我们自己完成,而是由OS来完成。
  • 2️⃣在客户端是不用listenaccept的,但是需要connect(建立连接)我们可以自定义连接策略(失败了重连几次)。

5.3 🍎tcpServer.cc🍎

#include<memory>
#include"err.hpp"
#include"tcpServer.hpp"string echoMssage(const string& message)
{return message;
}static void usage(string proc)
{std::cout << "Usage:\n\t" << proc << " port\n"<< std::endl;
}int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=stoi(argv[1]);unique_ptr<tcpServer> utcp(new tcpServer(echoMssage,port));utcp->init();utcp->start();return 0;
}

5.4 🍎验证🍎

在这里插入图片描述我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?
在这里插入图片描述我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。
当我们把最先通信的客户端干掉之后:
在这里插入图片描述消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。

来看看我们写的代码:
在这里插入图片描述当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在service中我们是死循环的读取和发送消息的,那么当有另外的客户端请求时就不会给新的客户端建立连接,自然就发不出去,收不到喽!处理方式有两种:

  1. 多进程
  2. 多线程

5.4.1 🍋多进程🍋

    void start(){while (true){// 1 获取连接 明确是哪一个client发送来的sockaddr_in client;socklen_t len;int sock = accept(_listensock, (sockaddr *)&client, &len);if (sock < 0){cerr << "accept fail" << strerror(errno) << endl;continue;}std::string clientip = inet_ntoa(client.sin_addr);uint16_t clientport = ntohs(client.sin_port);cout << "get new link success:" << sock << " form " << _listensock << endl;// 2 处理消息//service(sock, clientip, clientport);// 这样做当我们有多个client时会有什么问题?// 方案一:多进程 让子进程帮助我们执行servicepid_t pid = fork();if (pid < 0){close(sock);continue;}else if (pid == 0){// child 建议关掉_listensockclose(_listensock);service(sock, clientip, clientport);exit(0);}// parent 一定要关闭sock,否则就会造成文件描述符的泄漏close(sock);waitpid(id, nullptr, WNOHANG);if (ret == pid)std::cout << "wait child " << pid << " success" << std::endl;}

在这里插入图片描述这样我们就能够很好的处理了。
除此之外还有一种更为精妙的方式:
在这里插入图片描述我们可以再fork一下,当是父进程的时候就退出,执行到下面那肯定就是孙子进程,由OS领养,自然就不用关心回收状态了(OS会自动帮助我们回收)

当然,这还不是最好的方式,最好的方式我们可以使用下面的代码:

signal(SIGCHLD, SIG_IGN); // 推荐这样写

一行就搞定了,直接忽略掉子进程退出给父进程发送的消息。

5.4.2 🍋多线程🍋

            // 方案二:多线程pthread_t pid;TcpData *pdata = new TcpData(sock, clientip, clientport, this);pthread_create(&pid, nullptr, threadRoutine, pdata);}}static void *threadRoutine(void *args){pthread_detach(pthread_self());TcpData* pd=static_cast<TcpData*>(args);pd->_cur->service(pd->_sock,pd->_clientip,pd->_clientport);}

其中TcpData类:

class tcpServer;
class TcpData
{
public:TcpData(int sock, string &_clientip, uint16_t _clientport, tcpServer *cur): _sock(sock), _clientip(_clientip), _clientport(_clientport), _cur(cur){}int _sock;string _clientip;uint16_t _clientport;tcpServer *_cur;
};

这样当我们再次运行时:
在这里插入图片描述
显然此时已经能够成功运行了。除了服务端使用多线程外,客户端也可以用一个线程池来创建,总的来说实现起来这里也不算太难,有兴趣的小伙伴可以参考博主之前实现的【Linux:线程池】来改装一下,有问题可以私信博主。


6 🍑TCP协议通讯流程🍑

在这里插入图片描述
这张图大家目前应该是看不太明白的,其实没啥关系,上面讲解的内容在博主后面的文章中会给出详细的解释,这里大家只需要简单的了解下过程就好了。

服务器初始化:

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来。

建立连接的过程:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为 三次握手

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

这个断开连接的过程, 通常称为 四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的?

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

相关文章:

【网络编程】揭开套接字的神秘面纱

文章目录 1 :peach:简单理解TCP/UDP协议 :peach:2 :peach:网络字节序 :peach:3 :peach:socket编程接口 :peach:3.1 :apple:socket 常见API :apple:3.2 :apple:sockaddr结构:apple: 4 :peach:简单的UDP网络程序 :peach:4.1 :apple:基本分析:apple:4.2 :apple:udpServer.hpp(重点…...

MySQL 8.0 事务定义和基本操作

MySQL 事务&#xff08;Transaction&#xff09;的四大特性&#xff1a;A、C、I、D A、原子性&#xff1a;&#xff08;Atomicity&#xff09; 一个事务是不可分割的最小工作单位。 执行的事务&#xff0c;要么全部成功&#xff0c;要么回滚到执行事务之前的状态。 C、一致…...

项目经理必备:常用的项目管理系统推荐!

当我们成为项目负责人时&#xff0c;找到合适的工具来管理跟进项目&#xff0c;就成为了迫切需要解决的问题。一款优秀的工具&#xff0c;在项目的管理跟进中&#xff0c;起着极为重要的作用&#xff0c;一般可以付费购买专门的项目管理软件。 1.可快速切换查看不同角度的项目信…...

【香瓜说职场】信任危机(2022.08.19)

自从17年4月份开始辞职创业&#xff0c;已经5年零4个月了。今天跟大家聊一点不太正能量的事。 首先关于“要不要说些不好的”这件事&#xff0c;我爸妈常建议我不要把不好的事情写出来&#xff0c;因为觉得丢人、不体面、怕影响合伙人关系、影响同事关系。而我觉得如果只写好的…...

【Rust】Rust学习 第六章枚举和模式匹配

本章介绍 枚举&#xff08;enumerations&#xff09;&#xff0c;也被称作 enums。枚举允许你通过列举可能的 成员&#xff08;variants&#xff09; 来定义一个类型。首先&#xff0c;我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来&#xff0c;我们会…...

Win10安装GPU支持的最新版本的tensorflow

我在安装好cuda和cudnn后&#xff0c;使用pip install tensorflow安装的tensorflow都提示不能找到GPU&#xff0c; 为此怀疑默认暗转的tensorflow是不带GPU支持的。 在tensorflow官网提供了多个版本的GPU支持的windows的安装包 https://www.tensorflow.org/install/pip?hlz…...

20个Golang自动化DevOps库

探索 20 个用于简化任务和提高生产力的重要库。 Golang&#xff0c;也称为 Go&#xff0c;是一种静态类型、编译型编程语言&#xff0c;由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 设计。它于 2009 年推出&#xff0c;旨在解决其他编程语言的缺点&#xff0c;特…...

【WiFi】WiFi 6E最新支持的国家和频段

信道Map图 国家和频段 CountryStatus Spectrum Andorra Adopted Considering 5945-6425 MHz 6425-7125 MHz ArgentinaAdopted5925-7125 MHzAustralia Adopted Considering 5925-6425 MHz 6425-7125 MHz Austria Adopted Considering 5945-6425 MHz 6425-7125 MHz BahrainA…...

如何使用html,包括css,js 画思维导图?有哪些可用的方法?

首先&#xff0c;创建一个新的HTML文件&#xff0c;可以使用任何文本编辑器。在文件中添加必要的标签和结构来定义网页的内容和布局。 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Mind Map</title><link re…...

机器学习---梯度下降代码

1. 归一化 # Read data from csv pga pd.read_csv("pga.csv") print(type(pga))print(pga.head())# Normalize the data 归一化值 (x - mean) / (std) pga.distance (pga.distance - pga.distance.mean()) / pga.distance.std() pga.accuracy (pga.accuracy - pg…...

【VB6|第23期】原来Jet.OLEDB也可以读取新版.xlsx的Excel文件

日期&#xff1a;2023年8月11日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xf…...

通过控制ros节点的启停,软实现人工控制和紧急停止功能的图示

通过控制ros节点的启停&#xff0c;软实现人工控制和紧急停止功能的图示 实现原理简介&#xff1a; 人工控制的节点&#xff1a; 键盘节点 方向盘节点 自动控制的节点&#xff1a; movebase 导航 autoware 等 底盘节点&#xff1a; 差速底盘 阿克曼底盘 控制节点&#xff1…...

面试热题(滑动窗口最大值)

给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 输入&#xff1a;nums [1,3,-1,-3,5,3,6,7], k 3 输出&#xff1a;[3,3,5,…...

【代码】表格封装 + 高级查询 + 搜索 +分页器 (极简)

一、标题 查询条件按钮&#xff08;Header&#xff09; <!-- Header 标题搜索栏 --> <template><div><div class"header"><div class"h-left"><div class"title"><div class"desc-test">…...

ant.design 组件库中的 Tree 组件实现可搜索的树: React+and+ts

ant.design 组件库中的 Tree 组件实现可搜索的树&#xff0c;在这里我会详细介绍每个方法&#xff0c;以及容易踩坑的点。 效果图&#xff1a; 首先是要导入的文件 // React 自带的属性 import React, { useMemo, useState } from react; // antd 组件库中的&#xff0c;输入…...

Linux系统编程之信号(上)

一、信号概念 信号就是软件中断。每当程序收到一个信号&#xff0c;都需要按指定的方法去处理。以下是UNIX系统的信号表。 其中core表示产生一个复制了该进程内存映像的core文件&#xff0c;它保存了程序现场&#xff0c;可以使用gdb来调试。 二、signal() signal()函数用于改…...

23.Netty源码之内置解码器

highlight: arduino-light Netty内置的解码器 在前两节课我们介绍了 TCP 拆包/粘包的问题&#xff0c;以及如何使用 Netty 实现自定义协议的编解码。可以看到&#xff0c;网络通信的底层实现&#xff0c;Netty 都已经帮我们封装好了&#xff0c;我们只需要扩展 ChannelHandler …...

sigmoid ReLU 等激活函数总结

sigmoid ReLU sigoid和ReLU对比 1.sigmoid有梯度消失问题&#xff1a;当sigmoid的输出非常接近0或者1时&#xff0c;区域的梯度几乎为0&#xff0c;而ReLU在正区间的梯度总为1。如果Sigmoid没有正确初始化&#xff0c;它可能在正区间得到几乎为0的梯度。使模型无法有效训练。 …...

RabbitMQ 消息队列

文章目录 &#x1f370;有几个原因可以解释为什么要选择 RabbitMQ&#xff1a;&#x1f969;mq之间的对比&#x1f33d;RabbitMQ vs Apache Kafka&#x1f33d;RabbitMQ vs ActiveMQ&#x1f33d;RabbitMQ vs RocketMQ&#x1f33d;RabbitMQ vs Redis &#x1f969;linux docke…...

PHP实现在线进制转换器,10进制,2、4、8、16、32进制转换

1.接口文档 2.laravel实现代码 /*** 进制转换计算器* return \Illuminate\Http\JsonResponse*/public function binaryConvertCal(){$ten $this->request(ten);$two $this->request(two);$four $this->request(four);$eight $this->request(eight);$sixteen …...

[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?

&#x1f9e0; 智能合约中的数据是如何在区块链中保持一致的&#xff1f; 为什么所有区块链节点都能得出相同结果&#xff1f;合约调用这么复杂&#xff0c;状态真能保持一致吗&#xff1f;本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里&#xf…...

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

【生成模型】视频生成论文调研

工作清单 上游应用方向&#xff1a;控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

快刀集(1): 一刀斩断视频片头广告

一刀流&#xff1a;用一个简单脚本&#xff0c;秒杀视频片头广告&#xff0c;还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农&#xff0c;平时写代码之余看看电影、补补片&#xff0c;是再正常不过的事。 电影嘛&#xff0c;要沉浸&#xff0c;…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏

一、引言 在深度学习中&#xff0c;我们训练出的神经网络往往非常庞大&#xff08;比如像 ResNet、YOLOv8、Vision Transformer&#xff09;&#xff0c;虽然精度很高&#xff0c;但“太重”了&#xff0c;运行起来很慢&#xff0c;占用内存大&#xff0c;不适合部署到手机、摄…...

pycharm 设置环境出错

pycharm 设置环境出错 pycharm 新建项目&#xff0c;设置虚拟环境&#xff0c;出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...