Linux网络

发布于:2025-07-28 ⋅ 阅读:(23) ⋅ 点赞:(0)

网络协议

概念

协议是一种约定,计算机有很多层,每一层都要有自己的协议,比如说关于如何处理发来的数据的问题,就有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代码

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


网站公告

今日签到

点亮在社区的每一天
去签到