网络协议
概念
协议是一种约定,计算机有很多层,每一层都要有自己的协议,比如说关于如何处理发来的数据的问题,就有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<jsoncpp/json/json.h>
#include<string>
#include<iostream>
using 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,然后如果我们想对这个结构体进行设置的时候,需要包含头文件< netinet/in.h >,我们可以包含< arpa/inet.h >在这个头文件里,包含大小端的函数,也可以帮我们把字符串风格的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<sys/types.h>
#include<sys/socket.h>
#include"log.hpp"
#include<string>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SIZE 1024
enum{
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 #define INADDR_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<sys/types.h>
#include<sys/socket.h>
#include"log.hpp"
#include<string>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SIZE 1024
enum{
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 #define INADDR_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<sys/types.h>
#include<sys/socket.h>
#include"log.hpp"
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string>
#include<cstring>
using namespace std;
#define SIZE 1024
enum{
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代码
TCP
服务端
listen
监听窗口
TCP是面向连接的,TCP服务器一般都是比较被动的,一直处于一种等待连接到来的状态,listen用于监听
第一个参数就是我们创建的套接字
accept
第一个参数也是我们创建的套接字,第二三个参数都是输出型参数,让我们知道我们当前获取的连接的来源是什么,返回值是一个套接字,我们上面socket出来的套接字是用来bind监听之类的工作,用于获得底层的连接,并不是后来提供服务的,一般只有一个,被称为监听套接字。accept返回的套接字才是后来用来提供服务的,可以有多个,可以用以下指令测试服务器是否可以连通
telnet IP port
代码
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include"log.hpp"
#include<cstring>
#include<cstdio>
#include<string>
#include<netinet/in.h>
#include<unistd.h>
#include<arpa/inet.h>
using 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<sys/types.h>
#include"log.hpp"
#include<sys/socket.h>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<iostream>
using 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<iostream>
#include<vector>
#include<queue>
#include<unistd.h>
using 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<iostream>
#include<vector>
#include<ctime>
#include<cstdlib>
using 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/null
int 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将触发两次挥手。
每一个链接的建立都需要用结构体管理起来,所以就把对链接的管理转化为对链表的增删查改