Linux网络
网络协议
概念
协议是一种约定,计算机有很多层,每一层都要有自己的协议,比如说关于如何处理发来的数据的问题,就有http,https,ftp等协议,关于长距离传输数据丢失的问题,就有tcp协议,关于如何定位主机的问题,就有ip协议,在我们传输信息的时候,多余的东西就是协议报头,协议就是一个结构体对象,两边的主机都知道这个结构体,所以传输过去的时候另外一台主机就可以立刻认识。计算机的生产厂商有很多,操作系统也有很多,为了让这些厂商生产的电脑能够通信,就需要一个约定,这就是网络协议
协议分层
高内聚低耦合降低软件的维护成本,比如说在表示层出的错和其他层没关系,只需要修改表示层的bug即可
上三层压缩为一层,会话层和表示层交给应用层,其实我们接下来需要了解的就只有五层
每一个设备虽然操作系统不同,但都遵守下面这个层次结构,这样各种不同的设备才能通信,局域网是可以直接通信的,以太网是局域网中的其中一个网络协议,大部分局域网都使用以太网协议
数据从客户端到最地下的以太网驱动程序,每一层都需要添加每一层协议的报头,所以最终发送的报文就是:报文=报头+有效载荷(数据),比如说应用层的有效载荷就是我们需要传输的数据,传输层的有效载荷就是我们需要传输的数据+应用层的FTP协议,另外一边接受的时候也可以区分报头和有效载荷,并分离,这就是解包过程,所以通信的过程本身就是不断的封装和解包的过程,在解包的时候将有效载荷交付上层称为分用
数据链路层
每一台主机都有网卡,网卡有一个Mac地址,Mac地址只需要保证在局域网里的唯一性即可,每台主机只有一个Mac地址。每一个机器其实都可以接收到报文,在数据链路层的这一层可以解包出这个报文的目标主机和源主机,然后对报文里的ip信息对比看看是不是自己的主机ip,如果是就处理,不是就丢弃报文,而在数据链路层丢弃的数据,上层是不知道的。
任何一个时刻可以由多个主机接受局域网里发生的消息,但一个时刻只能允许一个主机发送消息,可以把局域网看成多台主机共享的临界资源。
在以太网通信的时候,由于是光电信号,就会发生数据碰撞问题,所以发送数据的主机要执行避免碰撞的算法,错峰发送。碰撞域表示在以太网里有可能发生数据碰撞的区域。
在通信的时候其实有一个设备称为交换机,如果判断两太通信的主机都在交换机的一侧,无论是正常传输还是数据发生碰撞,交换机都不会把数据继续传输到另外一侧,就不会让数据在更大的局域网里传输,通过这样划分碰撞域就可以减少数据碰撞的概率。
但这里存在安全问题,网卡分为普通模式和混杂模式,混杂模式下的网卡会把数据上传给上层,所以我们的数据需要加密。
令牌环网:和局域网一样,每一个时刻都只能有一个主机往令牌环网发送消息,只有具有令牌的主机才能往令牌环网发送消息,这个令牌类似于系统编程里面的锁的概念
IP层
Mac是应用于局域网,只在局域网里的唯一性,而IP地址是保证在全网里的唯一性的,其实在数据传输过程中有两套地址,Mac地址会一直根据目的地址而改变,而IP地址始终不变,现在的IP地址一般使用IPv4,代表IP地址有四字节,还有其他的,比如说IPv6等等,IPv6有128个比特位表示IP地址,大概16个字节
如果目标主机和源主机不在同一个子网,源主机就要先把数据交给路由器,路由器解包后,知道Mac的源地址和目标地址确认这个数据需要通过自己传出去,通过查自己的表才能转发到另外一台主机,路由器可以通过解包封装再次把报文传出去,这是把以太网协议转化为令牌环网协议,这样IP协议依靠路由器屏蔽了底层网络的差异化,而路由器需要搭配两张网卡才能实现这种功能
所以IP地址在传输过程中一般不会发生改变,但Mac地址在出局域网后会丢弃源和目的的地址,由路由器重新包装
ifconfig
基本概念
日常网络通信的本质是进程间通信,不同的是,系统编程里进程是在内存中传送数据的,网络通信是在网络协议栈中传输数据的,网络通信是在网络协议中的下三层(传输层,网络层,数据链路层)主要是用来把数据安全可靠的传输到远端机器,传输层需要把数据安全可靠的传到上层,主要是使用端口号,每一个软件都有不同的端口号,假设我们在一个应用客户端里要传输数据到服务端,就要把源端口号和目标端口号写到报文里,传输出去,这样服务端的传输层就能准确的把数据传输到服务端的上层,当我们要把数据从服务端传回去的时候,就要把源端口号和目标端口号倒一下即可
端口号
在公网上IP可以标识唯一一台主机,端口号port用来表示这台主机上唯一的进程,其实进程pid也可以标识进程的唯一性,但端口号的引入可以实现系统和网络的解耦,把数据从源主机传输到目标主机的传输层,需要进行一次哈希运算,把端口号和进程的task_struct映射,这样就能找到对应的进程,所以在cs结构里,每一个服务器(s端)对外的端口号都是确定的。
一个进程可以有多个端口号,只要保证在自底向上映射的时候是唯一的即可,一个端口号不能被多个进程共享
传输层协议
TCP协议
有连接,可靠传输,面向字节流,但维护成本高,因为在通信途中没有确认传输成功之前,TCP就需要把数据存在传输层维护起来。
自定义协议
比如说我们要实现一个网络计算器协议,客户端可以传输字符串,但不好读取,也可以传输结构体,但涉及内存对齐的问题,就会导致有些地方用不了
所以这里就涉及到序列化和反序列化的问题,序列化就是指我们把约定好的结构体转化为一个字符串,反序列化就是指我们把字符串转化为约定好的结构体,这也是OSI七层模型里的表示层,关乎我们自己定义的协议,socket套接字是传输层里的,代码里的TCP服务相当于是会话层,用于建立新链接
序列化和反序列化的代码位置
上述代码的序列化和反序列化其实可以使用JSON工具代替,但如果我们需要使用这个库的话,需要先安装
sudo yum install jsoncpp-devel -y
出现下图就安装成功了
#include#include#includeusing namespace std;int main(){ //序列化 Json::Value root; root[\"_size\"]=7; root[\"_a\"]=20; root[\"_op\"]=\'+\'; root[\"_b\"]=50; //value是万能对象,甚至可以套Json //Json::Value test; //eg.root[\"_test\"]=test; Json::FastWriter w; //Json::StyledWriter w;//可读性比较强 string ret=w.write(root); cout<<\"ret:\"<<ret<<endl; //反序列化 Json::Value v; Json::Reader r; r.parse(ret,v); int size=v[\"_size\"].asInt(); int a=v[\"_a\"].asInt(); int b=v[\"_b\"].asInt(); char op=v[\"_op\"].asInt(); cout<<\"size:\"<<size<<endl; cout<<\"a:\"<<a<<endl; cout<<\"op:\"<<op<<endl; cout<<\"b:\"<<b<<endl; return 0;}
使用这个库的时候,因为是.so,所以在编译的时候需要指定动态库
UDP协议
无连接,不可靠传输,面向数据报
网络字节序
网络数据流也有大小端之分,低位数据放在低位,高位数据放在高位则为小段,反之则为大端,但一台机器并不是固定大端或者小端,所以如果当前发送消息的主机是小端,就需要先将数据转为大端,发送主机通常会按内存地址从低到高发出
套接字
套接字编程包括域间套接字编程,主要是用于一个主机内的进程间通信,也是网络套接字的子集,网络套接字编程,主要是用于用户间的网络通信,原始套接字编程,主要是用于绕过传输层,直接使用网络层和数据链路层传输数据,通常用于编写一些网络工具,如果想将网络接口统一抽象化,参数的类型必须是统一的,我们会发现其实这三种并不一样,但我们接口只设计了第一种类型,因为我们在这里面设计了一个判断逻辑,如果前两个字节等于AF_INET,就变成第二种类型,如果等于AF_UNIX,就变成第三种类型,这样的接口就会变成通用的了
UDP
在云服务器本地的时候,是可以通过私有ip访问的,还有本地环回ip,但在其他机子上只能使用公网ip进行访问
创建套接字
第一个参数表示我们将来要创建的套接字的域,比如说AF_LOCAL表示域间套接字,我们一般使用AF_INET表示使用IPv4
第二个参数表示定义出来的套接字的类型,比如说SOCK_STREAM表示流式套接字,SOCK_DGRAM表示数据报套接字
第三个参数表示协议类型
返回值:如果申请成功,那么会返回一个文件描述符,如果创建失败则返回-1
绑定端口号
第二个参数传的是一个结构体struct sockaddr,我们可以使用结构体struct sockaddr_in,使用bzero/memset将这个结构体置为0,然后如果我们想对这个结构体进行设置的时候,需要包含头文件,我们可以包含在这个头文件里,包含大小端的函数,也可以帮我们把字符串风格的IP地址转为4字节的IP地址,比如说inet_addr这种函数。
当我们包含完头文件的时候,可以使用结构体,struct sockaddr有四个字段,sin_zero表示填充字段,没有实际意义
sin_addr表示当前主机ip,填充的是点分十进制表示的字符串ip地址,但我们一般传进来的都是字符串,所以我们需要把字符串转为整数,sin_addr是一个结构体,里面还有一个uint32_t的字段
//字符串切割//把字符串转为整数struct ip{uint8_t part1;uint8_t part2;uint8_t part3;uint8_t part4;};uint32_t host_ip;struct ip* x=(struct ip*)&host_ip;x->part1=stoi(\"111\");x->part2=stoi(\"222\");x->part3=stoi(\"33\");x->part4=stoi(\"44\");//如果ip要被网络使用,也必须要转化为网络序列,上述的part1到part4修改一下顺序就可以表示自己需要的序列//把整数转为字符串string host_ip=to_string(ip->part1)+\".\"+to_string(ip->part1)+\".\"+to_string(ip->part1)+\".\"+to_string(ip->part1)+\".\";
上述把字符串转为整数的也就是inet_addr这个函数
sin_family表示当前使用的域或者是协议家族,如下图,我们可以选择AF_INET,如果转向定义可以参考宏的概念的##
sin_port表示端口,但在传这个端口的机器有大端也有小端,我们可以通过调用以下的接口把主机字节顺序调整为网络字节顺序,h开头表示把主机字节顺序转化为网络字节顺序,n开头表示把网络字节顺序转化为主机字节顺序
第三个参数其实是这个结构体的大小
如果绑定成功,那么返回0,如果绑定失败返回-1,并且设置错误码
#pragma once#include#include#include\"log.hpp\"#include#include#include#include#define SIZE 1024enum{ Socketerror, Binderror, Recverror, Senderror};string defaultip=\"0.0.0.0\";uint32_t defaultport=3306;class UDPserver{public: UDPserver(const string ip=defaultip,uint32_t port=defaultport) :_ip(ip) ,_port(port) ,_isrunning(true) {} void Init() { //创建套接字 int socketfd=socket(AF_INET,SOCK_DGRAM,0); if(socketfd<0) { log(Fatal,\"socket fail,socket return a val is %d\\n\",socketfd); exit(Socketerror); } _socketfd=socketfd; log(Info,\"create socket success\\n\"); //绑定套接字 struct sockaddr_in structaddr; //sin_addr是一个结构体,里面有一个变量为s_addr,在赋值的时候需要转化为网络序列 structaddr.sin_addr.s_addr=htons(inet_addr(_ip.c_str())); structaddr.sin_addr.s_addr=INADDR_ANY;//表示ip地址为0x00000000 #defineINADDR_ANY((in_addr_t) 0x00000000) //sin_family有用到宏定义里的## structaddr.sin_family=AF_INET; //由于在传数据的时候也需要把自己的端口号传出去,所以也需要转化为网络序列 structaddr.sin_port=htons(_port); int n=bind(socketfd,(const struct sockaddr*)&structaddr,sizeof(structaddr)); if(n<0) { log(Fatal,\"bind fail,return val is %d\\n\",n); exit(Binderror); } log(Info,\"bind success\\n\"); } ~UDPserver() {}private: int _socketfd; string _ip; uint32_t _port; bool _isrunning;};
服务器
接收消息
recvfrom用于接收消息,第一个参数表示自己的套接字,第二,三个参数用于读取所需要的字段,第四个参数用于判断是否需要阻塞,如果为0则要阻塞,第五六个参数都是输出型参数,当接收成功的时候返回收到的字节数量,接收失败的时候返回-1
发送消息
sendto用于发送消息,第一个参数表示自己的套接字,第二,三个参数用于读取所需要的字段,第四个参数用于判断是否需要阻塞,如果为0则要阻塞,第五六个参数是输入型参数,传入的是当时接收消息的结构体和结构体大小,当发送成功的时候返回发送的字节数量,发送失败的时候返回-1
#pragma once#include#include#include\"log.hpp\"#include#include#include#include#define SIZE 1024enum{ Socketerror, Binderror, Recverror, Senderror};string defaultip=\"0.0.0.0\";uint32_t defaultport=3306;class UDPserver{public: UDPserver(uint32_t port=defaultport,const string ip=defaultip) :_ip(ip) ,_port(port) ,_isrunning(true) {} void Init() { //创建套接字 int socketfd=socket(AF_INET,SOCK_DGRAM,0); if(socketfd<0) { log(Fatal,\"socket fail,socket return a val is %d,error is %s\\n\",socketfd,strerror(errno)); exit(Socketerror); } _socketfd=socketfd; log(Info,\"create socket success\\n\"); //绑定套接字 struct sockaddr_in structaddr; //sin_addr是一个结构体,里面有一个变量为s_addr,在赋值的时候需要转化为网络序列 //structaddr.sin_addr.s_addr=htons(inet_addr(_ip.c_str())); structaddr.sin_addr.s_addr=INADDR_ANY;//表示ip地址为0x00000000 #defineINADDR_ANY((in_addr_t) 0x00000000) //sin_family有用到宏定义里的## structaddr.sin_family=AF_INET; //由于在传数据的时候也需要把自己的端口号传出去,所以也需要转化为网络序列 structaddr.sin_port=htons(_port); int n=bind(socketfd,(const struct sockaddr*)&structaddr,sizeof(structaddr)); if(n<0) { log(Fatal,\"bind fail,return val is %d,error is %s\\n\",n,strerror(errno)); exit(Binderror); } log(Info,\"bind success\\n\"); } void run() { char inbuffer[SIZE]; char outbuffer[SIZE]; while(_isrunning) { //接收消息 struct sockaddr_in recvstruct; socklen_t len=sizeof(recvstruct); cout<<\"run success,ip is \"<<_ip<<\"port is \"<<_port<<endl; int ret=recvfrom(_socketfd,inbuffer,sizeof(inbuffer),0,(struct sockaddr*)&recvstruct,&len); if(ret<0) { log(Warning,\"recv fail ,return value is %d,error is %s\\n\",ret,strerror(errno)); exit(Recverror); } log(Info,\"receive message success\\n\"); //设计一个echo //回发消息 ret=sendto(_socketfd,inbuffer,sizeof(inbuffer),0,(const sockaddr*)&recvstruct,len); if(ret<0) { log(Warning,\"send message fail,return value is %d,error is %s\\n\",ret,strerror(errno)); exit(Senderror); } log(Info,\"send message success\\n\"); } } ~UDPserver() {}private: int _socketfd; string _ip; uint32_t _port; bool _isrunning;};
#include\"UDPserver.hpp\"void usage(){ cout<<\"use:./UDPserver port\"<<endl;}int main(int argc,char* argv[]){ if(argc!=2) { usage(); exit(0); } uint16_t arg=stoi(argv[1]); UDPserver* server=new UDPserver(arg); server->Init(); server->run(); return 0;}
问题
ip问题
如果我们是在虚拟机上运行,这个代码是不会有错的,但云服务器会出错,因为云服务器禁止绑定公网IP,但可以绑定本地ip,如果我们绑定的ip地址是0,表示我们不会将这台ip地址动态绑定,所以发给这台主机的数据都可以通过端口号访问,根据端口号向上交付,所以这台机器如果有多个ip地址,就可以同时接收发往这些ip地址的消息,这也就是任意地址绑定
一些机器可能有多个ip,但如果本台机器只绑定了其中一个固定ip,那么这台机器就无法接收发往另外一个ip的消息。
端口号问题
当我们把端口号改成80,则会绑定失败,因为权限不够,系统里比较小的端口号一般要有固定的应用层协议,一般我们绑定端口号都要绑到1024以上,端口号一般是在0-65535之间
但如果我们使用root账户的权限还是可以绑定的
本地环回地址
这里的127.0.0.1就是本地环回地址,这个地址是可以在任意服务器下直接绑定的,如果主机绑定了这个地址,那么这个主机只能用于本地的进程间通信,也就是在主机的网络协议栈走了一遍,但并没有给我们推送到网络里,通常用于client-server的测试
客户端
客户端的端口号需要绑定,但不允许用户自主绑定,而是由系统自动分配,客户端的端口号是多少并不重要,只要保证唯一性即可,所以我们可以直接启动,用sendto直接向服务器发送报文,再用recvfrom接收即可。
可以使用Windows直接和Linux通信
#pragma once #include#include#include\"log.hpp\"#include#include#include#includeusing namespace std;#define SIZE 1024enum{ socketerror, sendtoserver, recvfromserver};class UDPclient{public: UDPclient(string ip,uint32_t port) :_serverip(ip) ,_serverport(port) ,isrunning(true) { int sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { log(Fatal,\"socket fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(socketerror); } _sockfd=sockfd; log(Info,\"socket success\\n\"); } void run() { struct sockaddr_in dest; dest.sin_addr.s_addr=inet_addr((_serverip.c_str())); dest.sin_family=AF_INET; dest.sin_port=htons(_serverport); socklen_t len=sizeof(dest); while(isrunning) { //发送消息 cout<<\"please enter#\"; cin.getline(sendbuffer,sizeof(sendbuffer)); int sendret=sendto(_sockfd,sendbuffer,sizeof(sendbuffer),0,(const struct sockaddr*)&dest,len); if(sendret<0) { log(Warning,\"send to server fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(sendtoserver); } cout<<sendbuffer<<endl; log(Info,\"send message to server success\\n\"); //接收消息 struct sockaddr_in src; socklen_t srclen=sizeof(src); int recvret=recvfrom(_sockfd,recvbuffer,sizeof(recvbuffer),0,(struct sockaddr*)&src,&srclen); if(recvret<0) { log(Warning,\"receive from server fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(recvfromserver); } log(Info,\"receive from server success\\n\"); cout<<recvbuffer<<endl; } }private: string _serverip; char sendbuffer[SIZE]; char recvbuffer[SIZE]; int _sockfd; uint32_t _serverport; bool isrunning;};
#include\"UDPclient.hpp\"void usage(){ cout<<\"./UDPclient destip destport\"<<endl;}int main(int argc,char* argv[]){ if(argc!=3) { usage(); exit(0); } //第二个参数是ip地址 string ip=argv[1]; //第三个参数为port端口 uint16_t port=stoi(argv[2]); UDPclient* client=new UDPclient(ip,port); client->run(); return 0;}
我们还可以获得客户端和服务端各自主机的ip地址和端口号,但我们会发现这个ip地址并不是点分十进制
使用inet_ntoa就可以把ip地址转化为点分十进制,这个函数会把空间开辟出来,用来存放返回的字符串,返回值是字符串的起始地址,因为这是静态产生的,所以不需要手动释放,但这并不能多次调用,比如说我们有一个地址是全0,另外一个地址是全F,先调用全0的inet_ntoa,再调用全F的inet_ntoa后就会导致两次得到的字符串都是255.255.255.255,这个函数在重复调用的时候会出现覆盖问题,还有线程安全问题,所以这个函数是可以使用的,但最好是使用inet_ntop函数
UDP代码
UDP代码请点击此处
TCP
服务端
listen
监听窗口
TCP是面向连接的,TCP服务器一般都是比较被动的,一直处于一种等待连接到来的状态,listen用于监听
第一个参数就是我们创建的套接字
accept
第一个参数也是我们创建的套接字,第二三个参数都是输出型参数,让我们知道我们当前获取的连接的来源是什么,返回值是一个套接字,我们上面socket出来的套接字是用来bind监听之类的工作,用于获得底层的连接,并不是后来提供服务的,一般只有一个,被称为监听套接字。accept返回的套接字才是后来用来提供服务的,可以有多个,可以用以下指令测试服务器是否可以连通
telnet IP port
代码
#pragma once#include#include#include#include\"log.hpp\"#include#include#include#include#include#includeusing namespace std;const int backlog=5;enum{ Sockerror=1, Listenerror, Accepterror, Readerror};class TCPserver{public: TCPserver(const string& ip,uint16_t port) :_ip(ip) ,_port(port) ,isrunning(true) { //创建监听套接字 listensocketfd=socket(AF_INET,SOCK_STREAM,0); if(listensocketfd<0) { log(Fatal,\"server socket fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Sockerror); } //绑定监听套接字 struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family=AF_INET; //server.sin_addr.s_addr=inet_addr(_ip.c_str()); inet_aton(_ip.c_str(),&(server.sin_addr)); //server.sin_addr.s_addr = INADDR_ANY; server.sin_port=htons(_port); socklen_t len=sizeof(server); bind(listensocketfd,(sockaddr*)&server,len); //监听 int retlisten=listen(listensocketfd,backlog); if(retlisten<0) { log(Fatal,\"listen fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Listenerror); } log(Info,\"listen success\\n\"); } void run() { while(isrunning) { //获取新链接 struct sockaddr_in client; socklen_t len=sizeof(client); int socketfd=accept(listensocketfd,(struct sockaddr*)&client,&len); if(socketfd<0) { log(Warning,\"accept fail,errno is %d,error is %s\\n\",errno,strerror(errno)); continue; } char buffer[1024]; inet_ntop(AF_INET,&(client.sin_addr),buffer,sizeof(buffer)); uint32_t clientport=ntohs(client.sin_port); log(Info,\"accept a new link success,client ip is %s,client port is %d\\n\",buffer,clientport); //close(socketfd); serverfunc(socketfd); } } void serverfunc(int socketfd) { char outbuffer[1024]; while(1) { memset(outbuffer,0,sizeof(outbuffer)); ssize_t ret=read(socketfd,outbuffer,sizeof(outbuffer)); if(ret<0) { log(Fatal,\"read fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Readerror); } else if(ret==0) { close(socketfd); } outbuffer[ret]=\'\\0\'; cout<<\"server say#\"<<outbuffer<<endl; string sendbuffer; sendbuffer+=outbuffer; write(socketfd,sendbuffer.c_str(),sendbuffer.size()); } }private: int listensocketfd; string _ip; uint16_t _port; bool isrunning;};
客户端
客户端无需手动bind端口号
connect
建立连接后就可以直接往自己的sockfd这个文件描述符对应的文件写数据,服务端就会收到,addr是目标服务器的sockaddr
代码
#pragma once#include#include\"log.hpp\"#include#include#include#include#include#includeusing namespace std;enum{ Socketerror, Connecterror, Writeerror};class TCPclient{public: TCPclient(string ip,uint16_t port) :_ip(ip) ,_port(port) ,isrunning(true) { //创建客户端套接字 _socket=socket(AF_INET,SOCK_STREAM,0); if(_socket<0) { log(Fatal,\"socket fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Socketerror); } //不需要显示bind //连接到服务端 struct sockaddr_in dest; memset(&dest,0,sizeof (dest)); dest.sin_port=htons(_port); dest.sin_family=AF_INET; inet_pton(AF_INET,_ip.c_str(),&(dest.sin_addr)); log(Info,\"socket success\\n\"); int n=connect(_socket,(struct sockaddr*)&dest,sizeof(dest)); if(n<0) { log(Fatal,\"connect fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Connecterror); } log(Info,\"connect success\\n\"); string sendbuffer; while(1) { cout<<\"please enter#\"; getline(cin,sendbuffer); write(_socket,sendbuffer.c_str(),sendbuffer.size()); char inbuffer[1024]; int ret=read(_socket,inbuffer,sizeof(inbuffer)); if(ret>0) { inbuffer[ret]=\'\\0\'; cout<<\"client receive#\"<<inbuffer<<endl; } } } void run() { while(isrunning) { //往文件描述符对应的文件写入数据 cout<<\"please enter# \"; string inbuffer=\"client send a message#\"; string tmp; cin>>tmp; inbuffer+=tmp; ssize_t ret=write(_socket,inbuffer.c_str(),inbuffer.size()); if(ret<0) { log(Fatal,\"write fail,errno is %d,error is %s\\n\",errno,strerror(errno)); exit(Writeerror); } inbuffer[ret]=\'\\0\'; } } ~TCPclient() { close(_socket); }private: int _socket; string _ip; uint16_t _port; char buffer[1024]; bool isrunning;};
问题
上述的客户端和服务器只能服务一个客户端,因为在服务一个服务端的时候,服务器就被阻塞在serverfunc里了
多进程
我们可以创建子进程对这些进行处理
void run() { while(isrunning) { //获取新链接 struct sockaddr_in client; socklen_t len=sizeof(client); int socketfd=accept(listensocketfd,(struct sockaddr*)&client,&len); if(socketfd<0) { log(Warning,\"accept fail,errno is %d,error is %s\\n\",errno,strerror(errno)); continue; } char buffer[1024]; inet_ntop(AF_INET,&(client.sin_addr),buffer,sizeof(buffer)); uint32_t clientport=ntohs(client.sin_port); log(Info,\"accept a new link success,client ip is %s,client port is %d\\n\",buffer,clientport); pid_t id=fork(); if(id==0) { close(listensocketfd);//子进程不关心 if(fork()>0) exit(0);//让父进程不阻塞的技巧,让孙子进程提供服务,最后回收子进程后,孙子进程变成孤儿进程 serverfunc(socketfd); close(socketfd); exit(0); } close(socketfd);//父进程不关心,因为有两份,只关闭了父进程那一份 //阻塞等待,但由于子进程刚打开就关闭了,所以就不会阻塞 //也可以使用SIG_IGN waitpid(id,nullptr,0); //close(socketfd); } }
多线程
这样做的话,每来一个客户,就会产生一个线程,但客户是会退出的,所以只要不遇到峰值,就不会有太大问题,可以应用于小型应用
void run() { while(isrunning) { //获取新链接 struct sockaddr_in client; socklen_t len=sizeof(client); int socketfd=accept(listensocketfd,(struct sockaddr*)&client,&len); if(socketfd<0) { log(Warning,\"accept fail,errno is %d,error is %s\\n\",errno,strerror(errno)); continue; } char buffer[1024]; inet_ntop(AF_INET,&(client.sin_addr),buffer,sizeof(buffer)); uint32_t clientport=ntohs(client.sin_port); log(Info,\"accept a new link success,client ip is %s,client port is %d\\n\",buffer,clientport); //多线程版 pthread_t tid; Pthread_data*data=new Pthread_data(_ip,_port,socketfd); pthread_create(&tid,nullptr,pthreadfunc,data); //pthread_join(tid,nullptr); delete data; }
线程池
可以把客户的要求分发给线程池里的多个线程
#pragma once#include#include#include#includeusing namespace std;template<class T>class pthread_pool{private: static const int max_size=10;public: static void* do_task(void* args)//如果是普通函数的话会多一个隐藏参数this指针,不匹配pthread_create第三个参数 { pthread_pool<T>* arg=static_cast<pthread_pool<T>*>(args); //上锁 pthread_mutex_lock(&(arg->lock)); //判断队列里有没有数据 while(arg->tasks.size()==0) { //条件等待 pthread_cond_wait(&(arg->cond),&(arg->lock)); } //处理 T task=arg->tasks.front(); arg->tasks.pop(); pthread_mutex_unlock(&(arg->lock)); //task.count(); //task.consumerprint(); task.serverfunc(); } void push(T task) { pthread_mutex_lock(&lock); tasks.push(task); //task.productorprint(); //通知 pthread_cond_signal(&cond); pthread_mutex_unlock(&lock); } static pthread_pool<T>* getinstance() { if(pdata==nullptr) { pthread_mutex_lock(&lock1); if(pdata==nullptr) { pdata=new pthread_pool<T>(); } pthread_mutex_unlock(&lock1); } return pdata; }private: pthread_pool() :maxsize(max_size) ,pthreads(maxsize) { //创建锁和条件变量 pthread_mutex_init(&lock,nullptr); pthread_cond_init(&cond,nullptr); //创建线程池 for(int i=0;i<maxsize;i++) { pthread_t tid; pthread_create(&tid,nullptr,do_task,this); pthreads.push_back(tid); } } pthread_pool<T>& operator=(const pthread_pool<T>& it)=delete; pthread_pool(const pthread_pool<T>& it)=delete; ~pthread_pool() { pthread_mutex_destroy(&lock); pthread_cond_destroy(&cond); }private: int maxsize; vector<pthread_t> pthreads; queue<T> tasks; pthread_mutex_t lock; pthread_cond_t cond; //创建单例模式 static pthread_pool<T>* pdata; static pthread_mutex_t lock1;};template<class T>pthread_pool<T>* pthread_pool<T>::pdata=nullptr;template<class T>pthread_mutex_t pthread_pool<T>::lock1=PTHREAD_MUTEX_INITIALIZER;
#pragma once#include#include#include#includeusing namespace std;enum{ normal=0, divzero, modzero, operator_error};class Task{public: Task(int socketfd) :_socketfd(socketfd) {} void serverfunc() { char outbuffer[1024]; while(1) { memset(outbuffer,0,sizeof(outbuffer)); ssize_t ret=read(_socketfd,outbuffer,sizeof(outbuffer)); if(ret>0) { outbuffer[ret]=\'\\0\'; cout<<\"server say#\"<<outbuffer<<endl; string sendbuffer; sendbuffer+=outbuffer; write(_socketfd,sendbuffer.c_str(),sendbuffer.size()); } } }private: int _socketfd;};
pthread_pool<Task>* pool=pthread_pool<Task>::getinstance();pool->push(Task(socketfd));
守护进程
TCP有一个特点,因为客户端和服务端的联系是依靠管道,当我们把读端关闭了之后,写端对应的进程就会收到一个SIGPIPE信号,也就是服务器进程,就会导致服务器进程退出,所以为了服务器不崩溃,需要对读端也做处理
signal(SIGPIPE,SIG_IGN);
TCPclient(string ip, uint16_t port) : _ip(ip), _port(port), isrunning(true) { while (isrunning) { int cnt = 5; bool isreconnect = false; do { // 创建客户端套接字 _socket = socket(AF_INET, SOCK_STREAM, 0); if (_socket < 0) { log(Fatal, \"socket fail,errno is %d,error is %s\\n\", errno, strerror(errno)); exit(Socketerror); } // 不需要显示bind // 连接到服务端 struct sockaddr_in dest; memset(&dest, 0, sizeof(dest)); dest.sin_port = htons(_port); dest.sin_family = AF_INET; inet_pton(AF_INET, _ip.c_str(), &(dest.sin_addr)); log(Info, \"socket success\\n\"); int n = connect(_socket, (struct sockaddr *)&dest, sizeof(dest)); if (n < 0) { log(Fatal, \"connect fail,errno is %d,error is %s,reconnect cnt is %d\\n\", errno, strerror(errno), cnt); isreconnect = true; cnt--; sleep(1); // exit(Connecterror); } else { isreconnect = false; } } while (cnt && isreconnect); if (cnt == 0) { exit(Connecterror); } log(Info, \"connect success\\n\"); run(); } }
但在这一份代码里,如果客户端还在访问服务器的时候,突然服务器出现问题了,客户端会进行重新连接,但重新连接不了新开的服务器,因为服务器的socket这些资源已经不同了,所以我们需要设置一个接口让这些资源可复用,下面的setsockopt可以用于防止偶发性的服务器无法立即重启的问题
这样即可重启
int opt=1;setsockopt(listensocketfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
之前有了解到的前台进程和后台进程,拥有键盘文件的就是前台进程,当我们想把一个后台进程提到前台的时候,可以使用
fg 后台任务号
如果我们想查看我们当前的后台任务,可以使用
jobs
如果我们想把前台任务放回后台,可以使用ctrl+z把前台进程暂停,系统就自动把bash提到前台,暂停的进程放在后台,因为必须要有一个前台进程使用键盘资源
如果我们想让因为暂停而被放在后台的进程继续执行,可以使用
bg 后台任务号
补充:
PGID是进程组ID,任务是用来指派给进程组的,TTY是进程对应当前显示器的文件,SID一样的表示在同一个会话(session)中启动执行的
所以一般会话退出的时候,会话的进程组也会受到影响,有时候bash退出了,一些后台进程并不会退出,而是变成孤儿进程,被系统领养,在第二次会话登录的时候依然还在,但父进程变成1,SID也变为?,所以这种进程是会收到会话的影响的,所以windows其实是有一个注销的功能的,注销就是用于将所有进程关闭,避免很多进程留在后台而导致卡顿。
而守护进程是不会收到会话变化的影响的,也就是不会收到登录和注销的影响,因为守护进程自成进程组,也自成会话,守护进程的本质其实也是孤儿进程
如果执行成功,就返回新的SID,但这里有一个问题,如果我们需要创建一个新的会话,那么这个进程不能是进程组的leader,但如果进程只有一个,那么这个进程很容易就变成这个进程组的leader,我们要怎么让当前进程不是leader呢?我们可以在执行代码的时候调用fork,父进程可能是leader,我们把父进程exit后,子进程就一定不会是组长,申请SID也就不会失败
//忽略其他异常信号signal(SIGPIPE,SIG_IGN);signal(SIGSTOP,SIG_IGN);//......if(fork()>0)exit(0);setsid();//更改工作目录//不一定需要一直在我们启动进程的目录chdir(/*路径*/);//方法1:关闭标准输入,标准输出,标准错误,但这个其实不太适用//方法2:/dev/null垃圾桶//如果直接关闭文件描述符,就会导致调用printf,cout这些函数全部出错,而我们又不可能在把一个进程变成守护进程的时候把所有的printf,cout等等删除//所以我们可以把需要打印的消息往/dev/null里打印,这样调用就不会出错了//用dup2把这三个重定向到/dev/nullint fd=open(\"/dev/null\",O_RDWR);dup2(fd,0),dup2(fd,1),dup2(fd,2);close(fd);//需要执行的代码
这样做,即使xshell关闭,其他主机也可以通过公网ip访问当前进程
如果我们不想自己写守护进程的代码,可以使用daemon
第一个参数如果设置为0,表示我们当前守护进程工作在根目录下,否则就使用当前工作目录,第二个参数如果是0,就会把当前标准输入,标准输出,标准错误重定向到/dev/null,如果不为0就不会重定向
简单原理
tcp是全双工的,因为tcp每一个socket都有一个接收缓冲区和发送缓冲区,不会造成混乱,客户端和服务端在发送消息的同时,也在接收消息。
tcp会通过三次握手来进行链接的建立,三次握手实际上是两个操作系统之间三次报文的传送,当我们调用connect后,只需要等待三次握手成功后connect返回,accept需要建立链接成功后才能返回,否则将阻塞,通过四次挥手来完成链接的释放,调用一个close将触发两次挥手。
每一个链接的建立都需要用结构体管理起来,所以就把对链接的管理转化为对链表的增删查改