文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

【网络编程】揭开套接字的神秘面纱

2023-08-31 07:53

关注

文章目录


TCP协议:

UDP协议:


我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

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

       #include               uint32_t htonl(uint32_t hostlong);       uint16_t htons(uint16_t hostshort);       uint32_t ntohl(uint32_t netlong);       uint16_t ntohs(uint16_t netshort);

3.1 🍎socket 常见API 🍎

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len); // 开始监听socket (TCP, 服务器)int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)int accept(int socket, struct sockaddr* address, socklen_t* address_len); // 建立连接 (TCP, 客户端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

3.2 🍎sockaddr结构🍎

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
在这里插入图片描述
所以当我们使用的时候可以将地址强转成 sockaddr* 类型。

struct sockaddr的定义:
在这里插入图片描述struct sockaddr_in的定义:
在这里插入图片描述


4.1 🍎基本分析🍎

在写之前,我们先来简单的分析分析下我们应该怎样写?首先我们封装一个udpServer的类来帮助我们创建套接字以及套接字的初始换工作,当然客户端也可以使用这种方式来完,不过由于客户端的代码很简单,我就不在封装一个udpClient的类了。
其次我们思考下udpServer类中成员应该有哪些?
首先肯定要一个套接字(其本质就是一个文件描述符),其次我们需要一个端口号,大家猜一下,我们需要一个IP地址吗?这个其实是不需要的,因为一款服务器/云服务器一般是不要指定某一个具体的IP地址的.
那我们bind的时候应该怎样传入参数呢?这个大家先不急,等会儿将代码写好了大家在回过来看就会清晰很多。为了方便使用我们还可以用一个包装器来包装我们将来要执行回调的函数。

4.2 🍎udpServer.hpp(重点)🍎

#pragma once #include#include           #include #include #include #include#include#includeusing namespace std;using fun_t =function<string(string)>;class udpServer{public:    const static uint16_t defaultPort=8848;    udpServer(fun_t service=nullptr, uint16_t port =defaultPort)    :_service(service)    ,_port(port)    {}    void init()    {        //1 创建套接字,打开网络文件        _socket=socket(AF_INET,SOCK_DGRAM,0);        if(_socket<0)        {            cerr<<"create socket fail"<<endl;            exit(-1);        }        //2 bind        sockaddr_in local;        memset(&local,0,sizeof(local));        local.sin_family=AF_INET;        local.sin_port=htons(_port);        local.sin_addr.s_addr=INADDR_ANY;        if(bind(_socket,(sockaddr*)&local,sizeof(local))<0)        {            cerr<<"bind fail"<<endl;            exit(-2);        }        cout<<"bind success"<<endl;    }    void start()    {        char buffer[1024];//自定义缓冲区        while(true)        {            //1 从客户端收消息            sockaddr_in client;//用作输出型参数,用来接受是哪个具体的客户端发送数据给服务端的            socklen_t len=sizeof(client);            int n=recvfrom(_socket,buffer,sizeof(buffer)-1,0,(sockaddr*)&client,&len);            if(n>0)                buffer[n]=0;            else                continue;           // cout<<"receive message success"<           string clientIp=inet_ntoa(client.sin_addr);           uint16_t clientPort=ntohs(client.sin_port);           cout<<clientIp<<"-"<<clientPort<<":"<<buffer<<endl;            //2 处理消息            string message=_service(buffer);                       //3 发送消息给客户端            if(sendto(_socket,message.c_str(),message.size(),0,(sockaddr*)&client,sizeof(client))<0)            {                cerr<<"send message fail"<<endl;                exit(-3);            }                        //cout<<"send message success"<        }    }private:    int _socket;    uint32_t _port;    fun_t _service;};

4.2.1 🍋注意事项🍋

 #include            #include 

但是sockaddr_in是定义在下面的头文件中的:

#include #include 

所以我们写套接字编程的时候,这四个头文件都要带上。

4.3 🍎udpClient.cc🍎

#include"udpServer.hpp"//./udpClient serverIp serverPortvoid usage(){    cout<<"Usage error\n\t"<<"serverIp serverPort"<<endl;    exit(-1);}int main(int argc,char*args []){    if(argc!=3)    {        usage();    }    string serverIp=args[1];    uint16_t serverPort=stoi(args[2]);    //1 创建套接字    int sock=socket(AF_INET,SOCK_DGRAM,0);    if(sock<0)    {        cout<<"create socket fail"<<endl;        exit(-1);    }    //2 client要不要bind呢?要不要自己bind呢?    //要bind 但是不要自己bind 操作系统会帮助我们做这件事情    // 2 明确server    sockaddr_in server;    memset(&server,0,sizeof(server));    server.sin_family=AF_INET;    server.sin_port=htons(serverPort);    server.sin_addr.s_addr=inet_addr(serverIp.c_str());    while(true)    {        //1 用户输入        string message;        cout<<"[grm]:";        getline(cin,message);        sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));                //2 接受服务端信息        char buffer[1024];        sockaddr_in tmp;        socklen_t len=sizeof(tmp);        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(sockaddr*)&tmp,&len);        if(n>0)        {            buffer[n]=0;            cout<<buffer<<endl;        }    }    return 0;}

4.3.1 🍋注意事项🍋

这个函数有两个作用:

  1. 将字符串类型转化成四字节的uint32_t类型的四字节整数;
  2. 将主机序列转化成网络序列。

与这个函数具有同种功能的函数还有inet_aton
在这里插入图片描述
而上面的inet_ntoa则是与inet_aton具有相反的功能。
除此之外,还有inet_ptoninet_ntop:
在这里插入图片描述在这里插入图片描述在这个系列的转换函数中不仅可以转换IPV4的地址,也可以转换IPV6的地址。

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
在这里插入图片描述man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
在这里插入图片描述因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?在APUE中, 明确提出inet_ntoa不是线程安全的函数;但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

4.4 🍎udpServer.c🍎

#include#include"udpServer.hpp"string dealMessage(const string& message){    return message;}void usage(){    cout<<"Usage error\n\t"<<"serverPort"<<endl;    exit(0);}//./udpServer serverPortint main(int argc,char* argv[]){    if(argc!=2)    {        usage();    }    unique_ptr<udpServer> udpSer(new udpServer(dealMessage,8848));    udpSer->init();    udpSer->start();    return 0;}

上述准备工作做好了后就可以来上手验证了:
注意我们在运行客户端的可执行程序时加上的IP地址可以直接是127.0.0.1(表示本机),如果想要其他主机也能够正确访问的话要加上服务端的IP,也就是我们购买云服务器的公网IP地址。

4.5 🍎如何关闭防火墙+验证🍎

如果使用了云服务器的公网IP地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:
在这里插入图片描述最后点击确认,就可以了,我们就发现列表中多出了两条:
在这里插入图片描述
到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:

在这里插入图片描述这样我们就完成了一个简易版本的UDP网络通信的代码了。

除此之外,我们还可以实现一个客户端把命令给服务端,然后服务端在帮助我们执行:

static bool isPass(const std::string &command){       bool pass = true;    auto pos = command.find("rm");    if(pos != std::string::npos) pass=false;    pos = command.find("mv");    if(pos != std::string::npos) pass=false;    pos = command.find("while");    if(pos != std::string::npos) pass=false;    pos = command.find("kill");    if(pos != std::string::npos) pass=false;    return pass;}// 让客户端本地把命令给服务端,server再把结果给你!// ls -a -lstd::string excuteCommand(std::string command) // command就是一个命名{    // 1. 安全检查    if(!isPass(command)) return "you are bad man!";    // 2. 业务逻辑处理    FILE *fp = popen(command.c_str(), "r");    if(fp == nullptr) return "None";    // 3. 获取结果了    char line[1024];    std::string result;    while(fgets(line, sizeof(line), fp) != NULL)    {        result += line;    }    pclose(fp);    return result;}

当我们运行时:
在这里插入图片描述不难发现已经验证成功了。
上述代码中我们简单介绍下popen函数:
在这里插入图片描述这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)

当然在客户端和服务端中我们修改代码为生产者消费者模型(具体实现可以让一个线程读取消息,另外一个线程收消息)由于同一个文件描述符可以同时被多个线程读取,所以这样设计是OK的。这里我就不实验了,大家有兴趣可以自行下去尝试。


TCP的网络程序大致框架与UDP类似,其中不同点我会放在后面一点一点给出解释。

5.1 🍎tcpServer.hpp(重要)🍎

#pragma once#include "err.hpp"#include #include  #include #include #include #include #include #include #include #include #include #include #includeusing namespace std;using func_t = function<string(const string &)>;static const int backlog = 32;class tcpServer{public:    tcpServer(func_t func, uint16_t port)        : _func(func), _port(port)    {    }    void init()    {        // 1 创建套接字        _listensock = socket(AF_INET, SOCK_STREAM, 0);        if (_listensock < 0)        {            cerr << "creat sock fail:" << strerror(errno) << endl;            exit(SOCK_ERR);        }        // 2 bind        sockaddr_in local;        memset(&local, 0, sizeof(local));        local.sin_family = AF_INET;        local.sin_port = htons(_port);        local.sin_addr.s_addr = INADDR_ANY;        if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0)        {            cerr << "bind fail" << endl;            exit(BIND_ERR);        }        // 3 listen        if (listen(_listensock, backlog) < 0)        {            cerr << "listen fail" << strerror(errno) << endl;            exit(LISTEN_ERR);        }    }    void service(int sock, const string &clientip, const uint16_t &clientport)    {        string who = clientip + "-" + std::to_string(clientport) + ":";        char buffer[1024];        while (true)        {            // 1 读取消息            ssize_t n = read(sock, buffer, sizeof(buffer) - 1);            if (n > 0)            {                buffer[n] = 0;                // 2 处理消息                string message = _func(buffer);                cout << who << message << endl;                // server 发送消息给 client                int n = write(sock, message.c_str(), message.size());                if (n < 0)                {                    cerr << "write fail" << strerror(errno) << endl;                    exit(WRITE_ERR);                }            }            else if (n == 0)            {                cout << "client:" << clientip << "-" << to_string(clientport) << "quit,server also quit" << endl;                close(sock);            }            else            {                cerr << "read fail" << strerror(errno) << endl;                exit(READ_ERR);            }        }    }    void start()    {        while (true)        {            // 1 获取连接 明确是哪一个client发送来的            sockaddr_in client;            socklen_t len;            int sock = accept(_listensock, (sockaddr *)&client, &len);            if (sock < 0)            {                cerr << "accept fail" << strerror(errno) << endl;                continue;            }            std::string clientip = inet_ntoa(client.sin_addr);            uint16_t clientport = ntohs(client.sin_port);            cout << "get new link success:" << sock << " form " << _listensock << endl;            // 2 处理消息            service(sock, clientip, clientport);          private:    int _listensock;    uint16_t _port;    func_t _func;};

5.1.1 🍋注意事项🍋

enum {    SOCK_ERR=1,    BIND_ERR,    USAGE_ERR,    LISTEN_ERR,    ACCEPT_ERR,    CONNECT_ERR,    WRITE_ERR,    READ_ERR,};

tcpClient.cc:

#pragma once#include #include  #include #include #include #include #include #include #include #include #include #include "err.hpp"using namespace std;static void usage(string proc){    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"              << std::endl;}int main(int argc,char*argv[]){    if(argc!=3)    {        usage(argv[0]);        exit(USAGE_ERR);    }    // 1 创建套接字    int sock = socket(AF_INET, SOCK_STREAM, 0);    if (sock < 0)    {        cerr << "creat sock fail:" << strerror(errno) << endl;        exit(SOCK_ERR);    }    //2 client要bind,但是是不需要我们自己bind的    //client需要listen和accept吗?答案是不需要的    //3 connect    string serverip=argv[1];    uint16_t serverport=stoi(argv[2]);    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));    int cnt = 5;    while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)    {        sleep(1);        cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;        if(cnt <= 0) break;    }    if(cnt <= 0)    {        cerr << "连接失败..." << endl;        exit(CONNECT_ERR);    }    char buffer[1024];    // 3. 连接成功    while(true)    {        string line;        cout << "Enter>>> ";        getline(cin, line);        write(sock, line.c_str(), line.size());        ssize_t s = read(sock, buffer, sizeof(buffer)-1);        if(s > 0)        {            buffer[s] = 0;            cout << "server echo >>>" << buffer << endl;        }        else if(s == 0)        {            cerr << "server quit" << endl;            break;        }        else         {            cerr << "read error: " << strerror(errno) << endl;            break;        }    }    close(sock);    return 0;}

5.2 🍎tcpClient.cc🍎

#pragma once#include #include  #include #include #include #include #include #include #include #include #include #include "err.hpp"using namespace std;static void usage(string proc){    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"              << std::endl;}int main(int argc,char*argv[]){    if(argc!=3)    {        usage(argv[0]);        exit(USAGE_ERR);    }    // 1 创建套接字    int sock = socket(AF_INET, SOCK_STREAM, 0);    if (sock < 0)    {        cerr << "creat sock fail:" << strerror(errno) << endl;        exit(SOCK_ERR);    }    //2 client要bind,但是不需要我们自己bind的    //client需要listen和accept吗?答案是不需要的    //3 connect    string serverip=argv[1];    uint16_t serverport=stoi(argv[2]);    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));    int cnt = 5;    while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)    {        sleep(1);        cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;        if(cnt <= 0) break;    }    if(cnt <= 0)    {        cerr << "连接失败..." << endl;        exit(CONNECT_ERR);    }    char buffer[1024];    // 3. 连接成功    while(true)    {        string line;        cout << "Enter>>> ";        getline(cin, line);        write(sock, line.c_str(), line.size());        ssize_t s = read(sock, buffer, sizeof(buffer)-1);        if(s > 0)        {            buffer[s] = 0;            cout << "server echo >>>" << buffer << endl;        }        else if(s == 0)        {            cerr << "server quit" << endl;            break;        }        else         {            cerr << "read error: " << strerror(errno) << endl;            break;        }    }    close(sock);    return 0;}

5.2.1 🍋注意事项🍋

5.3 🍎tcpServer.cc🍎

#include#include"err.hpp"#include"tcpServer.hpp"string echoMssage(const string& message){    return message;}static void usage(string proc){    std::cout << "Usage:\n\t" << proc << " port\n"              << std::endl;}int main(int argc,char*argv[]){    if(argc!=2)    {        usage(argv[0]);        exit(USAGE_ERR);    }    uint16_t port=stoi(argv[1]);    unique_ptr<tcpServer> utcp(new tcpServer(echoMssage,port));    utcp->init();    utcp->start();    return 0;}

5.4 🍎验证🍎

在这里插入图片描述我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?
在这里插入图片描述我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。
当我们把最先通信的客户端干掉之后:
在这里插入图片描述消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。

来看看我们写的代码:
在这里插入图片描述当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在service中我们是死循环的读取和发送消息的,那么当有另外的客户端请求时就不会给新的客户端建立连接,自然就发不出去,收不到喽!处理方式有两种:

  1. 多进程
  2. 多线程

5.4.1 🍋多进程🍋

    void start()    {        while (true)        {            // 1 获取连接 明确是哪一个client发送来的            sockaddr_in client;            socklen_t len;            int sock = accept(_listensock, (sockaddr *)&client, &len);            if (sock < 0)            {                cerr << "accept fail" << strerror(errno) << endl;                continue;            }            std::string clientip = inet_ntoa(client.sin_addr);            uint16_t clientport = ntohs(client.sin_port);            cout << "get new link success:" << sock << " form " << _listensock << endl;            // 2 处理消息            //service(sock, clientip, clientport);            // 这样做当我们有多个client时会有什么问题?            // 方案一:多进程 让子进程帮助我们执行service            pid_t pid = fork();            if (pid < 0)            {                close(sock);                continue;            }            else if (pid == 0)            {                // child 建议关掉_listensock                close(_listensock);                service(sock, clientip, clientport);                exit(0);            }            // parent 一定要关闭sock,否则就会造成文件描述符的泄漏            close(sock);            waitpid(id, nullptr, WNOHANG);            if (ret == pid)                std::cout << "wait child " << pid << " success" << std::endl;        }

在这里插入图片描述这样我们就能够很好的处理了。
除此之外还有一种更为精妙的方式:
在这里插入图片描述我们可以再fork一下,当是父进程的时候就退出,执行到下面那肯定就是孙子进程,由OS领养,自然就不用关心回收状态了(OS会自动帮助我们回收)

当然,这还不是最好的方式,最好的方式我们可以使用下面的代码:

signal(SIGCHLD, SIG_IGN); // 推荐这样写

一行就搞定了,直接忽略掉子进程退出给父进程发送的消息。

5.4.2 🍋多线程🍋

            // 方案二:多线程            pthread_t pid;            TcpData *pdata = new TcpData(sock, clientip, clientport, this);            pthread_create(&pid, nullptr, threadRoutine, pdata);        }    }    static void *threadRoutine(void *args)    {        pthread_detach(pthread_self());        TcpData* pd=static_cast<TcpData*>(args);        pd->_cur->service(pd->_sock,pd->_clientip,pd->_clientport);    }

其中TcpData类:

class tcpServer;class TcpData{public:    TcpData(int sock, string &_clientip, uint16_t _clientport, tcpServer *cur)        : _sock(sock), _clientip(_clientip), _clientport(_clientport), _cur(cur)    {    }    int _sock;    string _clientip;    uint16_t _clientport;    tcpServer *_cur;};

这样当我们再次运行时:
在这里插入图片描述
显然此时已经能够成功运行了。除了服务端使用多线程外,客户端也可以用一个线程池来创建,总的来说实现起来这里也不算太难,有兴趣的小伙伴可以参考博主之前实现的【Linux:线程池】来改装一下,有问题可以私信博主。


在这里插入图片描述
这张图大家目前应该是看不太明白的,其实没啥关系,上面讲解的内容在博主后面的文章中会给出详细的解释,这里大家只需要简单的了解下过程就好了。

服务器初始化:

建立连接的过程:

这个建立连接的过程, 通常称为 三次握手

断开连接的过程:

这个断开连接的过程, 通常称为 四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的?


来源地址:https://blog.csdn.net/m0_68872612/article/details/131932732

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯