网络通信--Linux

2023-05-16

文章目录

  • 网络通信的基础
    • 通信模型
    • IP地址和端口port
  • 网络套接字
    • 网络字节序
    • 初识UDP与TCP两种协议
    • sockaddr结构体家族
    • 认识一些网络常用基础函数
  • UDP实现简单通信
  • TCP实现简单通信
  • 总结

网络通信的基础

网络通信是建立在多层协议之下,最终利用数据传输线路进行数据通信。首先必须要认识到,一般我们所使用的应用软件如QQ、微信来进行通信时,数据并不是简简单单的直接通过我们的软件跑到对方的软件那么简单,中间会经过一系列的包装与解包装才行。这就涉及到了分层传输的概念,有 OSI七层模型 和 TCP/IP四层模型,其中OSI模型是逻辑上的模型,并不是特别实用,因此大多采用TCP/IP四层模型。

通信模型

OSI七层模型:

img

TCP/IP五层(四层)模型:

image-20230228215507999

image-20230228230112038

image-20230228232626122

上面的图可以看出数据在传输的时候确实是通过多层协议进行分层传输的。

那么我们作为程序员,需要关心链路层和网络层的细节吗?实际上是不需要的,主要原因在于我们程序员是搞代码的,我们不需要关注太底层的东西,程序员只需要考虑如何使用提供好的接口,完成传输层的任务就可以了,底层的细节还有整个信息在传输时的处理,全部都有写好的协议来帮我们处理。

那么就会有一个新的概念:协议

协议就是对数据的具体处理方式,面对一个信息,相应的协议会对它进行包装处理,以便于下一层理解与接受,并且适应于各种不同的信息,使得每一层信息的交流都像是在层层对话一样:在发送者看来,自己就是实打实的与接受者在交流,但是实际上是应用层通过协议把数据传给了下一层的传输层。此时对于传输层来说,又像是直接与接收者的传输层进行交流,实际上呢,又把信息再次加工传给了网络层……由此层层往下传,到最后就会传到最底层的物理层,实现真正意义上的信息交流,之后再一路向上把信息一步一步解包装给接收者的各层,最后呈现为应用层拿到了数据,接收者收到了信息,实现了完整的通信过程。

image-20230228235234237

上面我们提到了数据的包装以及解包装,实际上有更专业的叫法:封装分用

封装的过程就是数据一路到底的过程,分用的过程就是一路向上的过程,由于协议的不同,数据在每层被加入以及去除的信息也不同:

不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame).

应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation).首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息.

数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理

封装:

image-20230301002635717

分用:

image-20230301165536696

IP地址和端口port

通信的整体过程我们了解完毕,现在的重点在于如何实现具体两台或多台计算机之间的通信。就如我们对话一般,和对方讲话是需要先知道对方的存在的,对于计算机通信中的发送者而言,则是需要知晓自己发送信息的目的地(接收者的主机),而接收者也需要知道消息从哪里来的(发送者的主机),因此统一使用IP地址来标识一台计算机的主机。例如:127.0.0.1,格式就是####.####.####.####,有四个数字组成(每个数字只有1字节大小),并用小数点分隔开,也称“点分十进制”,大小总共是4个字节(主机转网络时的大小,后面会讲)。

端口唯一标识一个正在进行网络通信的进程。之所以要精准到进程是因为通信一般都是应用程序间的交流,要想实现通信的准确性,就必须要发送给对应主机的对应进程。端口号是一个2字节的无符号整形数字。

🔺注意端口号与进程PID的区别,PID是唯一标识一台计算机内的进程,而端口则是在进程通信的时候才会与对应的进程绑定在一起,两者不能等效。

网络套接字

网络套接字实际上就是一个结构体,里面需要我们配置各种网络传输的属性,这里有一些需要强调的相关知识点:网络字节序、UDP与TCP两种协议。

网络字节序

提到过我们程序员只负责传输层的代码实现,因此也就意味着我们需要网络层的IP,在传输层实现对端口的绑定以及数据的接收和发送工作,那么这里会自然而然涉及到数据的有序问题,我们知道计算机在存储数据时会分为大端和小端存储:

image-20230301173320067

发送者与接收者的存储形式若是出现了相反的情况,就会出现通信错误的情况。为了避免这种情况的出现,我们需要统一信息发送与接收的格式:大端

不管是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

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

image-20230301214812238

这几个函数都非常简单,意思也很好理解:h是host的缩写,代表主机序列;而n是network的缩写,代表网络序列;中间的to则是代表转换的方向。结尾的l代表是4字节的数据,s代表是2字节的数据。

例如:本地端口port转网络字节序

htons(port);//函数的返回值就是转化好的数据

初识UDP与TCP两种协议

UDP和TCP是两种不同的传输协议,在套接字中也有所体现,更直观的属性可以直接列个表更容易观察:

image-20230301214400178

UDP和TCP中TCP更加常用,网络传输中的稳定性是非常重要的一个因素。我们接下来可以先接触UDP的使用,原因在于TCP的使用其实是在建立在UDP上的基础之上。可以算是对UDP的完善吧,目前以我的认识是这样的。

sockaddr结构体家族

我们不是要谈套接字吗?这就来了!套接字地址是一个结构体,但是除了sockaddr,还有sockaddr_in和sockaddr_un这两种结构体,原因在于网络协议的不同(网络在发展的过程中的协议并不是都是同一的)。

image-20230301232131376

其中sockaddr_in用于网络通信,而sockaddr_un用于本地通信,那么第一个sockaddr是不是显得很多余?其实这样设计是有原因的:无论是本地通信还是网络通信,我们都希望可以使用同一套操作流程以及接口,那么就需要将不同的结构体进行处理,能够在一个接口里面被统一使用,不难观察,sockaddr_in和sockaddr_un的起始数据都是16位的地址,那么我们可以对他们的指针类型强转为sockaddr类型的,然后接口内部会通过判断开头的16位数据,决定到底是网络通信还是本地通信。

image-20230302165322123

上图是关于sockaddr_in的结构体源代码,观察里面的成员变量,其中sin_family就是域,决定是网络通信还是本地通信;sin_port为端口号;sin_addr为ip地址,不难看出最终该数据类型就是uint32_t的无符号四字节整形数字,也就是把点分十进制的ip由某个函数转成了数字,并且完成了字节序的处理工作。

上述涉及到ip地址转化的函数如下:

image-20230302170219084

函数名:inet_aton

参数:

​ cp:点分十进制类型的ip地址

​ inp: sockaddr_in结构中的sin_addr(ip地址的封装结构体)

功能:将点分十进制的ip放进对应的套接字中(本机转网络)

返回值:成功返回非零值;失败返回0。

函数名:inet_ntoa

参数:

​ in: sockaddr_in结构中的sin_addr(ip地址的封装结构体)

共能:将套接字中的四字节ip转化成点分十进制的ip地址(网络转主机)

返回值:点分十进制类型的ip地址

认识一些网络常用基础函数

函数:socketbindrecvfromsendtolistenacceptconnect

image-20230301234728019

函数名:socket(用于UDP/TCP)

功能:根据所传参数创建一个套接字(以某种方式读写的文件描述符),类似于缓存区。主要用来进行数据的传输和接收。

参数:

​ domain: AF_INET(网络通信)、AF_UNIX, AF_LOCAL(本地通信)

​ type: SOCK_DGRAM(UDP协议)、SOCK_STREAM(TCP协议)

​ protocol: 0(默认传值)

返回值:成功返回一个文件描述符;失败返回-1,并设置errno。

image-20230302000341869

函数名:bind(用于UDP/TCP)

功能:进程绑定端口号

参数:

​ sockfd: 套接字

​ my_addr:套接字地址结构体指针

​ addrlen: 套接字地址的大小

返回值:执行成功返回0,否则返回-1, 并设置错误代码。

image-20230302162722141

函数名:recvfrom(用于UDP)

参数:

​ sockfd: 套接字

​ buf: 用于存放接收数据的空间首地址

​ len: 存储空间大小

​ flags: 默认为0

​ addr: 远端套接字地址(强转成struct sockaddr*,该参数为输出型参数,获取远端套接字地址)

​ len: 远端套接字地址的大小(输出型参数)

功能:从远端获取信息通过sockfd(套接字)放到buf中

返回值:成功返回接收字节数,失败返回值-1,并设置errno

image-20230302162759468

函数名:sendto(用于UDP)

参数:

​ sockfd: 套接字

​ buf: 发送数据的空间首地址

​ len: 发送数据的大小

​ flags: 默认为0

​ addr: 远端套接字地址(强转成const struct sockaddr*,该参数为输入型参数,注意区分于recvfrom函数)

​ addr_len: 远端套接字地址的大小

功能:通过sockfd(套接字)向远端发送buf内len大小的信息

返回值:成功返回发送的字节数,失败返回值-1,并设置errno

image-20230302223758027

函数名:listen(用于TCP)

参数:

​ s:监听套接字(是服务端用来专门监听的套接字)

​ backlog:指定未完成连接队列的最大长度.如果一个连接请求到达时未完成,连接队列已满,那么客户端将接收到错误 ECONNREFUSED. 或者,如果下层协议支持重发,那么这个连接请求将被忽略,这样客户端在重试的时候就有成功的机会。

返回值:函数执行成功时返回0.错误时返回-1,并置相应错误代码 errno

功能:在一个套接字上倾听连接(和UDP主要的不同点:有连接特性!)

image-20230302225547896

函数名:accept(TCP)

参数:

​ s:监听套接字

​ addr:远端套接字地址(强转成struct sockaddr*,该参数为输出型参数,获取远端套接字地址)

​ addrlen: 远端套接字地址的大小(输出型参数)

返回值:此调用在发生错误时返回-1.若成功则返回一个非负整数标识这个连接套接字。(只用于与远端的通信的套接字)

功能:获取远端网络信息和用于连接的套接字

image-20230303004631417

函数名:connect(用于TCP)

参数:

​ sockfd:连接套接字

​ addr: 远端套接字地址(强转成const struct sockaddr*,该参数为输入型参数,注意区分于listen函数)

​ addr_len: 远端套接字地址的大小

返回值:函数执行成功时返回0.错误时返回-1,并置相应错误代码 errno

功能:连接远端与一个套接字

上面介绍的函数中:recvfrom、sendto函数(UDP) 与 accept、connect函数(TCP)有些许相似之处,要注意区分。

UDP实现简单通信

在实现的时候,会出现许多需要记录日志的地方,因此在写有关通信的代码之,先搞一个日志输出的头文件:

//Log.hpp
#pragma once
#include <cstdio>
#include<ctime>
#include<cstdarg>
#include<cassert>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#define DEBUG 0    //测试
#define NOTICE 1   //注意
#define WARINING 2 //警告
#define FATAL 3    //错误
const char* log_level[]={"DEBUG","NOTICE","WARINING","FATAL"};
void logMessage(int level,const char* format,...) //可变参数,可以不用懂,会用就行
{
    assert(level>=DEBUG);
    assert(level<=FATAL);
    char* name=getenv("USER"); //获取用户的用户名
    char logInfo[1024];
    va_list ap;
    va_start(ap,format);

    vsnprintf(logInfo,sizeof(logInfo)-1,format,ap);


    va_end(ap);
    FILE* out=(level==FATAL)?stderr:stdout;

    fprintf(out,"%s | %u | %s | %s\n",\
    log_level[level],(unsigned int)time(nullptr),name==nullptr?"unknow":name,logInfo);//最终的输出样式
}

实现的模型:客户端与服务端的通信

服务端:

//udpServer.cc

//需要用到的头文件
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include "Log.hpp"
using namespace std;
static void Usage(const string proc)  //使用手册
{
    cout << "Usage:\n\t" << proc << "port [ip]" << endl;
}
class UdpServer//封装服务器
{
private:
    int sockfd_;//套接字
    uint16_t port_;//端口号
    string ip_;  //IP地址
    unordered_map<string,struct sockaddr_in> users_; //客户端ip+port 与 套接字地址的关联,记录已经通信的客户(客户表)
public:
    UdpServer(int port, string ip = "") : ip_(ip), port_(port), sockfd_(-1)//初始化端口和ip地址,套接字设为-1
    {
    }
    ~UdpServer() {}

public:
    void checkOnlineUser(string& ip,uint32_t port,struct sockaddr_in peer)//检测发送消息的客户端是否是新客户,不是则加入客户表中
    {
        string key=ip;
        key+=":";
        key+=to_string(port);
        auto it=users_.find(key);
        if(users_.end()==it)
        {
            users_.insert({key,peer});
        }
        else
        {
            //do nothing
        }

    }
    void messageRoutine(string& ip,uint32_t port, string info)
    {
        string message ="[";
        message+=ip;
        message+=":";
        message+=to_string(port);
        message+="]:";
        message+=info;
        for(auto& user:users_)//消息广播,所有的用户都可以收到消息
        {
            sendto(sockfd_,message.c_str(),message.size(),0,(const struct sockaddr*)&(user.second),sizeof(user.second));
        }
    }
    void init()//服务器启动之前的初始化
    {
        // 创建套接字,SOCK_DGRAM模式用于实现UDP通信
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "%s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        logMessage(NOTICE, "socket create success: %d", sockfd_);

        // 绑定网络,填充基本信息到结构为sockaddr_in的local套接字地址中
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//清空结构体
        local.sin_family = AF_INET; // 填充协议家族,域;AF_INET表示网络通信
        local.sin_port = htons(port_); // 填充服务器端口号
        // 填充IP,IP为空的话就接受任意IP地址远端的消息(INADDR_ANY)
        local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); 
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)//套接字、套接字地址与服务器进程进行绑定
        {
            logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
            exit(2);
        }
        logMessage(NOTICE, "socket bind success: %d", sockfd_);
    }
    void start()//启动服务器
    {
        char inbuffer[1024];
        char outbuffer[1024];
        struct sockaddr_in peer; // 远端(对服务器来说就是客户端)
        socklen_t len = sizeof(peer);
        while (true)//死循环轮询是否有客户端发送消息
        {
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s >= 0)
            {
                inbuffer[s] = 0;//有消息,加上'\0'是为了以字符串的形式去使用信息
            }
            else if (s == -1)
            {
                logMessage(WARINING, "recvfrom: %s", strerror(errno));
                continue;//没有信息,继续轮询检测
            }
            string peerIp = inet_ntoa(peer.sin_addr);//从peer套接字地址(客户端)中获取远端IP,调用关于IP地址从网络转主机的函数。
            uint32_t peerPort = ntohs(peer.sin_port);//从peer套接字地址(客户端)中获取远端的port
            checkOnlineUser(peerIp,peerPort,peer);//查看客户表,不在就将其加入
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);
            string info(inbuffer,inbuffer+s);//使用string容器构造信息
            messageRoutine(peerIp,peerPort,info); //信息输出(广播)
        }
    }

};
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]); //获取端口
    string ip;
    if (argc == 3)
    {
        ip = argv[2];//获取ip地址
    }
    UdpServer svr(port, ip); //服务端的实例化
    svr.init();  //服务端的初始化
    svr.start(); //服务端的开始
    
    return 0;
}

客户端:

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cassert>
using namespace std;
static void Usage(string name)
{
    cout << "Usage:\n\t" << name << " server_ip server_port" << endl;
}
void *recveAndPrint(void *args)
{
    while (true)//一直处于轮询检测状态
    {
        int sockfd = *(int *)args;
        char rev[1024];
        struct sockaddr_in tmp;//由于只用到了消息,没用到远端的基本信息,这里仅仅只是为了满足函数的参数要求
        socklen_t len = sizeof(tmp);
        ssize_t s = recvfrom(sockfd, rev, sizeof(rev) - 1, 0, (struct sockaddr *)&tmp, &len);
        if (s > 0)
        {
            rev[s] = 0;
            cout << "server echo# " << rev << endl;//输出收到的消息
        }
    }
    return nullptr;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string server_ip = argv[1];//服务端的IP地址
    uint16_t server_port = atoi(argv[2]);//服务端的端口
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);//创建套接字,SOCK_DGRAM模式用于实现UDP通信
    assert(sockfd > 0);

    struct sockaddr_in server;//填充套接字
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    pthread_t t;
    pthread_create(&t, nullptr, recveAndPrint, (void *)&sockfd);//创建一个线程用于信号的接收
    
    
	//客户端不需要绑定自己的端口和IP地址:由于一台计算机可能会有多个客户端同时运行,主动绑定端口可能会出现端口重复的问题,所以一般都是再发送信息的时候计算机自动帮我们用户绑定的。
    string buffer;
    while (true)
    {
        usleep(100);
        cout << "please enter# ";
        getline(cin, buffer);
        sendto(sockfd, buffer.c_str(), buffer.size(), 0, (const struct sockaddr *)&server, sizeof(server));
    }
    return 0;
}

大致流程:

image-20230302225332516

先跑一下代码看看效果:

开启三个终端,分别运行服务器和两个客户端

image-20230302192238413

客户端数据的广播与客户输入提示符有点冲突,但是并不影响整体的功能实现。

在运行服务器的时候,并没有直接绑定确定的ip端口,事实上这样做是被推荐的。接受任意IP地址的信息是一种比较好的处理方式。(实际上云服务器也并不支持确定IP地址的绑定操作)而客户端连接的IP地址为127.0.0.1,这个IP地址有点特殊,称之为本地环回地址,可直接用于一台主机的两个不同进程间的网络通信,也可以用真实的服务器IP地址去访问服务器,若是两台不同的计算机之间通信,就必须要用真实的IP地址了。

TCP实现简单通信

这里同样是建立服务器与客户端之间的通信,但是不同于UDP那么简单了,在绑定操作之后还有监听和接收两个操作。在实现TCP时,预备添加更复杂一点的操作,比如添加任务类、日志记录、服务器进程更改为后台进程(守护进程或者精灵进程)、线程池的处理需求方式等等。

其中需要点明的一个知识点就是后台进程,所谓的后台进程,不与任何控制终端相关联,并且周期性地执行某种任务或等待处理某些发生的事件(处理一些系统级的任务)。这样我们一旦启动服务器进程,就会直接脱离当前终端,不会再输出任何运行服务器的日志,转而我们可以设计把日志保存到一个文件中。我这个程序是在云服务器下运行的,这种情况下即使退出了整个终端,服务器仍然会在后台运行,客户端仍可以继续连接服务器使用服务。如果再加入信息广播,那么这就是个简易版的聊天室。

那么既然谈到了后台进程,就得考虑如何将服务端进程转化为后台进程了:

//daemonize.hpp
#pragma once
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize()
{
    int fd = 0;
    //忽略一些信号
    signal(SIGPIPE,SIG_IGN);
    
    //创建进程后直接结束父进程
    if(fork()>0)
    {
        exit(0);
    }
    //调用setsid()函数,使得子进程成为一组进程的组长
    setsid();
    
    //打开特殊文件“/dev/null”,相当于回收站,一切输入的数据都会被忽略
    if((fd=open("/dev/null",O_RDWR))!=-1)
    {
        //三次重定向使得所有的输出都指向回收文件
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
        if(fd>2) close(fd);//关闭特殊文件描述符,避免文件描述符泄露
    }
}

上面这个函数调用完之后就会使得进程成为后台进程。

接下来任务类

//Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "Log.hpp"
using namespace std;

class Task
{
public:
    using callback_t = function<void (int, string, uint16_t)>;//方法函数的返回值是void,参数是int、string、uint16_t
    Task() : sock_(-1), port_(-1)
    {}
    Task(int sock, string ip, uint16_t port, callback_t func)//任务的构造函数
        : sock_(sock), ip_(ip), port_(port), func_(func)
    {}
    ~Task()
    {}
    void operator()()//重载(),进行任务的处理
    {
        logMessage(DEBUG, "线程[%p]处理 %s[%d]的请求, begin...", pthread_self(), ip_.c_str(), port_);
        func_(sock_, ip_, port_);
        logMessage(DEBUG, "线程[%p]处理 %s[%d]的请求, end...", pthread_self(), ip_.c_str(), port_);
    }
private:
    int sock_;//网络套接字
    string ip_;//IP地址
    uint16_t port_;//端口
    callback_t func_; // 任务的回调函数
};

单例线程池

//ThreadPool.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <string>
using namespace std;
#define NUM 10
template <class T>
class ThreadPool
{
private:
    ThreadPool(const int &threadNum = NUM) : threadNum_(threadNum), isStart_(false)
    {
        assert(threadNum_ > 0);
        //初始化锁和条件变量
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;     // 删除拷贝构造
    void operator=(const ThreadPool<T> &) = delete; // 删除赋值构造
public:
    ~ThreadPool()
    {
        //销毁锁和条件变量
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    int threadNum()//获取线程个数
    {
        return threadNum_;
    }
    static ThreadPool<T> *getInstance() // 申请类的对象
    {
        pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
        if (instance == nullptr)
        {
            pthread_mutex_lock(&mutex);
            if (instance == nullptr)
            {
                instance = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&mutex);
        }
        return instance;
    }
    static void *threadpool(void *arg) //线程的回调函数
    {
        pthread_detach(pthread_self());//线程分离
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(arg);
        while (true)//抢任务
        {
            tp->lockQueue();
            while (!tp->haveTask())
            {
                tp->waitForTask();
            }
            T tmp = tp->pop();
            tp->unlockQueue();

           tmp();//处理任务
        }
        return nullptr;
    }
    void start()//线程池初始化,创建若干个线程
    {
        assert(!isStart_);
        for (int i = 0; i < threadNum_; ++i)
        {
            pthread_t tmp;
            pthread_create(&tmp, nullptr, threadpool, this);
        }
    }
    void put(const T &data)//放入任务
    {
        lockQueue();
        taskQueue_.push(data);
        unlockQueue();
        choiceThreadForHandler();//选择一个线程来处理任务
    }

private:
    void lockQueue() { pthread_mutex_lock(&mutex_); }
    void unlockQueue() { pthread_mutex_unlock(&mutex_); }
    bool haveTask() { return !taskQueue_.empty(); }
    void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
    void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
    T pop()//删除一个任务
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }

private:
    bool isStart_;//线程池是否已经启动
    int threadNum_;//线程个数
    queue<T> taskQueue_;//任务队列
    pthread_mutex_t mutex_;//锁
    pthread_cond_t cond_;//条件变量
    static ThreadPool<T> *instance;//单例指针
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr; // 懒汉模式

日志输出:(与UDP的日志输出不同点在于输出文件改成了日志文件)

//Log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define LOGFILE "serverTcp.log"//日志文件名
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    char *name = getenv("USER");
    char logInfo[1024];
    va_list ap;
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    va_end(ap);
    FILE *out = (level == FATAL) ? stderr : stdout;
    umask(0);
    int fd = open(LOGFILE, O_CREAT | O_APPEND | O_WRONLY, 0666);//以只写的方式追加打开日志文件
    assert(fd >= 0);

    //对标准输出和标准错误流进行重定向,指向日志文件
    dup2(fd, 1);
    dup2(fd, 2);
    fprintf(out, "%s | %u | %s | %s\n",
            log_level[level], (unsigned int)time(nullptr), name == nullptr ? "unknow" : name, logInfo);

    fflush(out); // 将C缓冲区中的数据刷新到OS
    fsync(fd);   // 将OS中的数据尽快刷盘
    close(fd);
}

服务端与客户端共同使用的头文件集合

//util.hpp
#pragma once
using namespace std;
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "Log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "daemonize.hpp"
#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5

#define BUFFER_SIZE 1024

服务端:

//serverTCP.cc
#include "util.hpp"
void execCommand(int sock, const string &clientIp, uint16_t clientPort)//程序替换功能函数
{
    assert(sock > 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);
    char command[BUFFER_SIZE];
    while (true)
    {
        ssize_t s = read(sock, command, sizeof(command) - 1);//通过read直接从连接套接字中读取信息
        if (s > 0)
        {
            command[s] = '\0';
            logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);
            string safe = command;
            if ((string::npos != safe.find("rm")) || (string::npos != safe.find("unlink")))
            {
                logMessage(WARINING,"This is not a safe command!");
                continue;
            }
            FILE *fp = popen(command, "r");//调用popen函数直接实现程序替换,并使得输出结果存储在fp指向的文件中,r为文件的读权限
            if (fp == nullptr)
            {
                logMessage(WARINING, "%s,exec %s failed", strerror(errno), command);
                break;
            }
            char message[BUFFER_SIZE];
            while (fgets(message, sizeof(message) - 1, fp) != nullptr)//从fp中拿取程序替换结果到message中
            {
                write(sock, message, strlen(message));//将程序替换的结果通过连接套接字写回到远端
            }
            pclose(fp);//关闭因程序替换而打开的文件描述符
            logMessage(DEBUG, "[%s:%d] exec [%s] done...", clientIp.c_str(), clientPort, command);
        }
        else if (s == 0)
        {
            logMessage(DEBUG, "client quit-->%s[%d]", clientIp.c_str(), clientPort);
            break;
        }
        else
        {
            logMessage(DEBUG, "client %s[%d],read: %s", clientIp.c_str(), clientPort, strerror(errno));
            break;
        }
    }
    close(sock);//关闭连接套接字
}
class ServerTcp
{
private:
    uint16_t port_;//端口号
    string ip_;//IP地址
    int listenSock_;//监听套接字
    ThreadPool<Task> *tp_;//线程池指针
public:
    ServerTcp(uint16_t port, const string ip = "")//初始化各成员变量
        : ip_(ip), port_(port), listenSock_(-1)
    {}
    ~ServerTcp()
    {}
    void init()
    {
        // 创建监听套接字,SOCK_STREAM决定了是TCP通信
        listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            logMessage(FATAL, "sock %s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMessage(DEBUG, "sock %s:%d", strerror(errno), listenSock_);
        // 填充基本的网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 绑定套接字与网络信息
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind %s", strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "bind %s:%d", strerror(errno), listenSock_);
        // 监听套接字
        if (listen(listenSock_, 5) < 0)
        {
            logMessage(FATAL, "listen %s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen %s:%d", strerror(errno), listenSock_);

        // 获取线程池对象(单例模式)
        tp_ = ThreadPool<Task>::getInstance();
    }
    void loop()
    {
        // 启动线程池
        tp_->start();
        logMessage(DEBUG, "ThreadPool start success, threadNum is %d", tp_->threadNum());

        // 死循环响应客户端网络请求
        while (true)
        {
            // 为获取远程网络信息做准备
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            
            //使用accept函数,拿到连接套接字
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);

            //通过套接字地址peer获得远端IP和port
            string peerIp = inet_ntoa(peer.sin_addr);
            uint16_t peerPort = ntohs(peer.sin_port);

            if (serviceSock < 0)
            {
                logMessage(FATAL, "accept %s[%d]", strerror(errno), serviceSock);
                continue;
            }
            logMessage(DEBUG, "accept %s | %s[%d], sock: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);
            Task t(serviceSock, peerIp, peerPort, execCommand);//创建任务
            tp_->put(t);//往线程池里面放入任务,线程池会自动进行任务处理
        }
    }
};
static void Usage(std::string proc)//使用手册
{
    std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"
              << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    string ip;
    if (argc > 2)
        ip = argv[2];
    daemonize();    //使进程变成后台进程
    ServerTcp svr(port, ip);
    svr.init();//服务端初始化
    svr.loop();//开始服务
    return 0;
}

客户端

//clientTcp.cc
#include "util.hpp"
volatile bool quit = false;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
    std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
              << std::endl;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);
    int sock=socket(AF_INET,SOCK_STREAM,0);// 创建套接字,SOCK_STREAM决定了是TCP通信
    if(sock<0)
    {
        cerr<<"socket: "<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverPort);
    inet_aton(serverIp.c_str(),&server.sin_addr);
    if(connect(sock,(const struct sockaddr*)&server,sizeof(server))!=0)//与远端建立连接(TCP特点)
    {
        cerr<<"connnect: "<<strerror(errno)<<endl;
        exit(CONN_ERR);
    }
    cout<<"client connnect success,sock: "<<sock<<endl;
    string message;
    while(!quit)
    {
        message.clear();
        cout<<"please enter# ";
        getline(cin,message);
        if(strcasecmp(message.c_str(),"quit")==0)
        {
            quit=true;
        }
        ssize_t s=write(sock,message.c_str(),message.size());//客户端写入数据
        if(s>0)
        {
            message.resize(1024);
            ssize_t s=read(sock,(void*)message.c_str(),1024);//拿取从远端获取的信息反馈
            if(s>0)
            {
                message[s]='\0';
            }
            cout<<"server echo# "<<message.c_str()<<endl;
        }
        else if(s<=0)
        {
            break;
        }
    }
    close(sock);//释放套接字的文件描述符
    
    return 0;
}

上面的代码看起来很吓人,其实都是一些比较基础的操作的结合,只不过是内容稍微丰富了一点,可以慢慢看着代码注释琢磨。

运行实例:

image-20230303010650741

运行结果符合总体预期。

除了本地环回地址,实名IP地址也可以访问:

image-20230303011132506

总结

这一篇的知识比较多,但是核心内容就是围绕套接字的网络内容,熟悉之后就能很快熟练掌握。实在有不懂的地方,可以在评论区提问,大家一起学习讨论。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

网络通信--Linux 的相关文章

  • 速腾Helios-16P使用Lego-loam实时构建点云地图(三)——使用雷达实时运行lego-loam

    当确定好雷达正常运行 lego loam正常运行后 xff0c 就可以开始使用雷达实时运行lego loam了 步骤一 xff1a 连接硬件 步骤二 xff1a 修改电脑IP地址 用完雷达记得将设置中有线地址选择之前的默认地址 xff0c
  • 串口通信协议【I2C、SPI、UART、RS232、RS422、RS485、CAN、TTL、USB】

    xff08 1 xff09 I2C 集成电路互连总线接口 Inter IC xff1a 同步串行半双工传输总线 xff0c 连接嵌入式处理器及其外围器件 支持器件 xff1a LCD驱动器 Flash存储器 特点 有两根传输线 xff08
  • Pyhton request模拟浏览器登录(过程、我踩过的坑及解决方式)

    过程 xff08 后表示代码中的操作 xff09 f12 gt network gt ctrl 43 R 登录 gt 输入账号 密码 gt 确定 在network中找到login 需要它的 请求标头 和 url xff09 准备 sessi
  • SpringSecurity用户密码验证过程

    SpringSecurity过滤链当中的UsernamePasswordAuthenticationFilter负责登陆密码验证 AbstractAuthenticationProcessingFilter是UsernamePassword
  • 项目实战:基于 TCP 的局域网内高性能文件传输系统设计与实现

    0 写在前面1 系统设计目标2 系统整体设计思路 2 1 网络传输协议的选择与通信协议的设计2 2 数据库设计 3 上传 下载文件的设计方案4 断点续传的原理及设计5 秒传的原理及设计6 数据库设计 API 编程与 shell 脚本的结合7
  • 进程与线程学习心得

    一 进程与线程的区别 1 进程是操作系统进行资源调度和分配的基本单位 线程是操作系统可执行的最小调度和分配单位 2 一个线程属于一个进程 一个进程可以有多个线程 3 一个进程崩溃不影响其他进程 但是一个线程崩溃会让进程崩溃 4 进程在执行过
  • 《网络编程》C语言 创建TCP服务器(三种)

    一 循环服务器 伪代码 xff1a sfd 61 socket bind listen while 1 newfd 61 accept while 1 recv send close newfd
  • 《网络编程》C语言 使用select函数搭建TCP客户端和服务器

    IO 多路复用概念 1 允许同时多个 IO 进行操作 xff0c 内核一旦发现进程执行一个或多个 IO 事件 xff0c 就会通知该进程 2 应用程序中同时需要处理多路输入输出流 select 功能 xff1a 让内核监听指定集合中的文件描
  • ubuntu下切换python版本(python2与python3之间的切换,python3与python3之间的切换)

    目录 1 问题2 重装python3 83 配置 python3 8 为系统默认 python34 切换回系统自带的python3 1 问题 有点无语 xff0c python3 8明明下载安装好 但是设置python默认版本为python
  • Ubuntu22.04中解决mNetassist无法打开问题

    因为本人在安装mNetassist遇到很多问题 xff0c 也是查找了很多资料 xff0c 最终解决了问题 因为每个人遇到的问题会有所不同 xff0c 故把本人的安装经历记录下来 xff0c 以供参考 大家可参考的文章是这篇 xff0c 但
  • C++中常用的库函数 (自用)

    常用的库函数 一 前言二 内容1 sort 题目 2 upper bound 与lower bound 题目 3 to string 4 string 内嵌的 find 函数 注 xff1a vector无find 函数5 大小写转换 to
  • 四轴飞行器PID调参建议

    在动态控制中 xff0c 我们通过调整PID三个参数来获得动力 xff0c 同时消除振荡 xff0c 找到对你当前的飞行场景来说更优的手感 P xff08 Propotional xff09 是比例的简称 P 单元控制着控制系统的所有动力
  • C语言string库strcpy、strcmp、strcat函数详解

    strcpy 即string copy 语法格式为strcpy str1 str2 作用是将str2赋值给str1 使用方法类似于 char str1 10 str2 61 34 abc 34 strcpy str1 34 bcd 34 s
  • VScode常用快捷键、

    VScode常用快捷键 xff1a 英文 按回车enter xff1a 会快速打出html 后缀名 自行填写 shift xff0b alt xff08 鼠标放在复制行代码区 xff0c 或者鼠标选择区域 xff09 按控制 向下 键 xf
  • 深入了解运行时栈(C语言)

    文章目录 运行时栈函数的栈帧寄存器与机器指令寄存器 xff1a 机器指令 程序计数器控制转移数据传送参数的传递返回值的传递 举例 xff1a 函数栈帧创建和销毁的全过程小结 运行时栈 栈是一种数据结构 xff1a 我们可以向这种结构中存入数
  • 小四轴调试记录

    从准备理论到实际动手调试大约耗时半年吧 xff0c 期间看了很多理论知识 xff0c 惯性导航方面的文章 至于为什么选择从小四轴入手 xff0c 当时的理由很简单 xff1a 1 便宜 xff0c 2 空心杯电机虽然有刷会坏但便宜 xff0
  • 登录 账号密码验证

    lt DOCTYPE html gt lt html lang 61 34 en 34 gt lt head gt lt meta charset 61 34 UTF 8 34 gt lt meta http equiv 61 34 X U
  • 关于C++变量重复定义

    本人是刚入学的大一计算机类学生 xff0c 最近在学习C 43 43 xff0c 在回顾这个代码时候发现 xff0c 这个重复定义i和j会导致之前定义的全局变量i和j并不能起作用 xff0c 现在还不太清楚为什么 xff0c 请小伙伴们注意
  • OpenMV——色块识别

    OpenMV有很多示例代码 xff0c 下面是我学习过程中有关知识的总结 目录 前言 一 阈值选择 二 代码 前言 函数RGB 255 0 0 表示的是红色 RGB 255 0 0 含义 xff1a 红色值 Red 61 255 xff1b
  • 链表的概念以及相关基础操作

    前言 xff1a 链表是数据结构里面最开始的章节 xff0c 也是对新手的理解有困难的第一章 笔者大二下学校才开设数据结构 xff0c 以防自己忘记 xff0c 遂记录之 链表的概念 xff1a 链表是一种物理存储单元上非连续 非顺序的存储

随机推荐

  • Sqoop数据导入 第2关:Mysql导入数据至HDFS上

    为了完成本关任务 你需要掌握 1 数据库 MySQL 建表 2 Mysql 数据导入至 HDFS 中 数据库 MySQL 建表 用命令进入 MySQL 客户端 mysql uroot p123123 h127 0 0 1 创建数据库hdfs
  • 一、单链表创建以及操作

    链表操作全内容 xff1a 1 xff0c 链表创建 2 xff0c 创建头结点 xff08 赋值 xff09 3 xff0c 输出当前链表 4 xff0c 连接节点形成链表 5 xff0c 插入节点函数 6 xff0c 删除节点函数 7
  • STL常用的容器

    STL常用容器 目录 STL常用容器vector容器pair c 43 43 内置二元组 不需要头文件stringqueue 队列priority queue 优先队列stack 栈deque 双端队列 速度异常的慢set map mult
  • Ubuntu网页连接失败的解决方法

    问题如下 xff1a 一 首先你要先检查自己的Ubuntu是否能连接上网络 二 在确保自己能连接上网络时 xff0c 网页连接失败的原因可能是你的Firefox浏览器的默认搜索引擎时Google xff0c 只需要在网页设置 搜索 中将搜索
  • Hal库_stm32开发之串口IDLE空闲中断+DMA接收不定长数据并修改数据

    本文写自于博主编写平衡智能车程序的时候 xff0c 由于中断使用的较多 xff0c 如定时器中断 xff0c 串口中断等等 为了避免MCU运行程序时卡死 xff0c 我就将串口中断改为串口IDLE空闲接收中断 可以实现不定长数据的接收 xf
  • PCB学习(一)——立创EDA边框设置

    一 伴随原理图转PCB生成的边框 通过在 原理图界面 点击 设计 下的 原理图转PCB xff08 Alt 43 P xff09 xff0c 可以得到如下 矩形边框 二 PCB界面设置边框 在 PCB界面 点击 工具 下的 边框设置 xff
  • c++模板类/模板函数的声明与定义应该放在头文件里

    c 43 43 模板类 模板函数的声明与定义应该放在头文件里 xff0c 不要分开来写类中函数的声明与定义 xff08 比如在 H文件里声明某个成员函数 xff0c 在 CPP文件里定义该成员函数 xff09 xff0c 这样会导致连接错误
  • STM32的GPIO端口配置八种模式的理解

    文章目录 一 GPIO的结构框图二 八种模式1 模拟输入 xff08 GPIO Mode AIN xff09 2 浮空输入 xff08 GPIO Mode IN FLOATING xff09 3 上拉输入 xff08 GPIO Mode I
  • ESP32 micro-usb 多数据量并简单加密的串口通信

    文章目录 前言一 ESP32和上位机的环境二 上位机部分1 串口通信的准备2 数据的准备与发送 三 下位机部分1 ESP32串口通信准备2 数据的接收四 同时测试 总结 前言 最近在研究单片机与上位机的串口通信 xff0c 刚好手头有一块E
  • boost之跨平台 错误处理

    system C 43 43 中处理错误的最佳方式是使用异常 xff0c 但操作系统和许多底层AP工不具有这个能力 xff0c 它们一般使用更通用也更难以操作的错误代码来表示出错的原因 xff0c 不同的操作系统的错误代码通常不是兼容的 x
  • 记录一下vector基本用法(简单易懂)

    vector容器的初始化 vector的使用首先需要加一个头文件 include lt vector gt xff1b vector lt int gt a 最一般的初始化方法 xff0c 就是定义一个容器啊a xff1b vector l
  • MPU6050基本原理介绍及程序配置

    一 MPU6050简介 1 内部主要结构 xff1a 陀螺仪 加速度计 数字运动处理器DMP xff08 Digital Motion Processor xff09 PS MPU6050还含有第二IIC接口 xff0c 用于连接一个 第三
  • Robomaster上位机视觉摘要——比赛规则篇

    本文是笔者多日来总结的2023赛季中针对上位机组的比赛规则摘要 xff0c 力求一文让你看懂上位机在赛场中的飒爽身影 目录 电力元件 电池 遥控器 激光 涂装 机器人 飞镖 雷达 空中机器人 工程机器人 哨兵机器人 英雄与步兵机器人 视觉应
  • C语言:结构体——关于内存字节对齐图文详解

    前言 xff1a 我们在学到c语言内存管理的时候总是一遍惊叹 xff0c 其聪明的内存管理策略 xff0c 一遍抱怨其难以理解的方法 xff0c 网上的资料要不讲究的太详细 xff0c 要不没能讲解清楚 xff0c 今天我们根据实例来学习一
  • 结构体+联合体 详解

    文章目录 一 结构体1 结构体变量2 特殊声明3 结构体的引用1 嵌套调用2 自引用 三 结构体的初始化四 结构体的内存对齐1 用法2 练习题3 修改对齐数 五 位段1 用法2 练习题 六 联合体1 用法2 练习题1 正常算法题2 用联合体
  • Ubuntu20.04——一篇文章让你从零配置VINS_Mono环境以及运行(2023年最新)

    注 xff1a 文末包含该文章涉及的所有安装包的网盘链接 零 换源 xff08 也可以先不换 xff0c 后面觉得下载慢再换也行 xff09 1 备份原来的源 sudo cp etc apt sources list etc apt sou
  • 学C语言推荐的书和软件—C Primer Plus和Dev C++

    写这个的目的是复习巩固C Primer Plus的知识 xff0c 我会一直更新这个系列 对于这本书 xff0c 入门C语言是完全够了 xff0c 后面的链表 队列和二叉树比较综合 xff0c 难度大一些 用这本书学C语言非常好 xff0c
  • mavlink python

    from pymavlink import mavutil Create the connection m 61 mavutil mavlink connection 39 udpin 0 0 0 0 14550 39 dir m mav
  • C++Vector浅析,Vector用法大全

    vector基本概念 功能 xff1a vector数据结构和数组非常相似 xff0c 也成为单端数组 vector与普通数组的区别 xff1a 不同之处在与数组是静态空间 xff0c 而vector可以动态扩展 动态扩展 xff1a 并不
  • 网络通信--Linux

    文章目录 网络通信的基础通信模型IP地址和端口port 网络套接字网络字节序初识UDP与TCP两种协议sockaddr结构体家族认识一些网络常用基础函数 UDP实现简单通信TCP实现简单通信总结 网络通信的基础 网络通信是建立在多层协议之下