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端)对外的端口号都是确定的。
一个进程可以有多个端口号,只要保证在自底向上映射的时候是唯一的即可,一个端口号不能被多个进程共享
0-1023是知名端口号,像http,ftp等应用层协议是固定的,ssh服务器使用22端口,ftp使用21端口,telnet服务器使用23端口,从1024-65535是操作系统动态分配的端口号,可以让普通用户进行绑定,下面这个指令可以查看服务器对应端口号

vim /etc/services

在这里插入图片描述

netstat

netstat -选项 #可以查看本地通信和网络通信
#n 拒绝显示别名,能显示数字的全部转化成数字
#l 仅列出有在 Listen (监听) 的服务状态
#p 显示建立相关链接的程序名
#t (tcp)仅显示tcp相关选项
#u (udp)仅显示udp相关选项
#a (all)显示所有选项,默认不显示LISTEN相关

iostat可以查看网络io的使用情况

pidof

pidof 进程名

可以用来把与服务名相关的进程的pid显示出来

传输层协议

在TCP/IP协议中,用“源IP”,“目的IP”,“源端口”,“目的端口”,“协议号”这样的五元组来表示一个通信,udp和tcp都是全双工的(在接收信息的时候可以同时发送消息)

TCP协议

有连接,可靠传输,面向字节流,但维护成本高,因为在通信途中没有确认传输成功之前,TCP就需要把数据存在传输层维护起来。
在这里插入图片描述
发送数据实际上和把数据刷新到磁盘一模一样,在用户层都有缓冲区,然后用户层缓冲区把数据交给操作系统内核的缓冲区,操作系统决定把数据交给底层的磁盘,就叫刷新到磁盘,如果交给底层的网卡,就叫做网络通信,但本质都是拷贝。
TCP的文件描述符只有一个,但这一个文件描述符既可以读,也可以写,因为TCP的两端的文件描述符对应的struct file都有两个缓冲区,一个用于读,一个用于写,所以TCP是可以控制发送的,等操作系统觉得什么时候可以发送了,数据才能发送,而udp做不到这一点,因为udp没有发送缓冲区

报文格式

在这里插入图片描述
前20字节属于tcp的标准报头
数据偏移(首部长度):表示报头长度+选项长度是多少,如果没有选项,那么这个值就是20,但首部长度只有4位,表示范围是0-15,但计算的单位是4字节,所以表示的范围是0-60字节,而标准报头是20字节,所以选项最多是40字节,然后通过固定长度分离
16位窗口大小:基于确认应答机制,当服务器给客户端确认应答的时候会发送完整报文或者报头,服务器会在窗口填上服务器接收缓冲区的剩余大小,同理,当客户端确认应答的时候也可以使用同样的方法
序号:
确认序号:

流量控制

服务器是有接收缓冲区和发送缓冲区的,当客户端不断向服务器发送消息,但服务器来不及处理的时候,会选择让客户端发慢一点,从而有时间处理接收缓冲区里的内容,这就是流量控制,否则会导致大面积丢包。而tcp是可以重传的,即使不进行流量控制,丢包之后也可以让客户端重新发送报文,但这样会消耗大量的网络带宽资源,造成低效率的问题

确认应答机制

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协议

无连接,不可靠传输,面向数据报
UDP报头+有效载荷
在这里插入图片描述
16位UDP长度指的是整个报头的长度,16位有效载荷的长度指的是数据的长度,也就是UDP长度-8
不可靠传输:如果UDP检验失败,会直接把报文丢弃,并不会通知对方再发送一次,也就是没有重传机制
面向数据报:如果需要传输一个10kB的数据,sendto传一次,那么recvfrom也只能接收一次,而不能循环调用10次recvfrom,每次1kB
UDP是没有发送缓冲区的,调用sendto直接交给链路层,只有接收缓冲区,如果接收缓冲区满了,后来的数据报会直接被丢弃,而且UDP不保证可靠性,可能传输来的数据报顺序是乱的
UDP的长度最多是2^16B,也就是64KB,不能通过UDP发送超过64KB的数据,比较常用于直播,视频等内容

struct udp_header
{
uint16_t src_port;
uint16_t dest_port;
uint16_t udp_len;
uint16_t check;
};

udp的报文是用一个称为sk_buff的结构体描述的

struct sk_buff
{
//struct udp_header | 需要发送的数据 | 其他
char* start;
//指向结构体的开头
char* pos;
//指向报文的有效部分
char* end;
//指向结构体的结尾
.....
struct sk_buff* next;
//指向下一个报文
};

DNS等应用层协议底层就是基于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代码

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将触发两次挥手。
每一个链接的建立都需要用结构体管理起来,所以就把对链接的管理转化为对链表的增删查改
在这里插入图片描述

posted @ 2025-08-01 21:17  yjbjingcha  阅读(6)  评论(0)    收藏  举报