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

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: 【网络编程】揭开套接字的神秘面纱(二)

4.4 🍎udpServer.c🍎

#include<memory>
#include"udpServer.hpp"
string dealMessage(const string& message)
{
    return message;
}
void usage()
{
    cout<<"Usage error\n\t"<<"serverPort"<<endl;
    exit(0);
}
//./udpServer serverPort
int 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地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:

32800d09140742d096c6ac556f490956.png

最后点击确认,就可以了,我们就发现列表中多出了两条:


43205c601c234a3cbfd841e4ab529599.png

到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:


f5bc28ee66084232aefdd58b88a216db.gif

这样我们就完成了一个简易版本的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 -l
std::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;
}

当我们运行时:


b511ab3e9140448a88a655ecdc110d7a.png

不难发现已经验证成功了。

上述代码中我们简单介绍下popen函数:


5f7eb6005a794c87aef5e53c465c651d.png

这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)

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

5 🍑简单的TCP网络程序 🍑

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

5.1 🍎tcpServer.hpp(重要)🍎

#pragma once
#include "err.hpp"
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include<signal.h>
using 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 🍋注意事项🍋

1️⃣由于是TCP,所以我们创建套接字时必须使用SOCK_STREAM.

2️⃣由于TCP是保证可靠性的面向字节流的可靠协议,所以TCP在使用上肯定会比UDP复杂得多,会多上listen(监听) 和 accept (获取连接)。在linten接口的创建中我们使用的第二个参数backlog我们将放在后面再讲解,这里不太好解释。accept接口的返回值也是一个套接字,这个套接字的任务是专门用来帮助我们读取和接受消息用的,而类中的_listensock套接字的作用主要是进行前面套接字的创建和初始化工作。(可以简单的理解为_listensock就相当于餐厅里在外面招呼客人的服务员,accept接口的返回值套接字就是为客户真正意义上做饭的厨师)

3️⃣我们将处理消息封装在了一个接口service中,在里面我们可以清晰得看见,读取消息用的是read,发送消息用的是write,这正是我们学习文件操作时所用到得系统调用,这也很好的印证在LINUX下一切皆文件的思想。

4️⃣ 代码中所存在的错误都用了错误码来标识,错误码可参考下面:

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

tcpClient.cc:

#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#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 <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#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 🍋注意事项🍋

  • 1️⃣与UDP类似在bind的时候需要bind,但是这个工作不由我们自己完成,而是由OS来完成。
  • 2️⃣在客户端是不用listenaccept的,但是需要connect(建立连接)我们可以自定义连接策略(失败了重连几次)。

5.3 🍎tcpServer.cc🍎

#include<memory>
#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 🍎验证🍎

image.png


我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?


5d5d5fe5427e42be8ed08ff1c07deb2f.png

我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。

当我们把最先通信的客户端干掉之后:


image.png

消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。

来看看我们写的代码:

image.png

当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在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;
        }

e6ad3c386727490299415a8dd5c7aed1.png

这样我们就能够很好的处理了。

除此之外还有一种更为精妙的方式:


c10cac18d5574e059d13995fd2a65d81.png

我们可以再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;
};

这样当我们再次运行时:


b78f20988e774e1cb2446b216582a938.png

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

6 🍑TCP协议通讯流程🍑

79d0c0b880e04fa8907ef04c5330e2c1.png

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

服务器初始化:

调用socket, 创建文件描述符;

调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;

调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

调用accecpt, 并阻塞, 等待客户端连接过来。

建立连接的过程:

调用socket, 创建文件描述符;

调用connect, 向服务器发起连接请求;

connect会发出SYN段并阻塞等待服务器应答; (第一次)

服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)

客户端收到SYN-ACK后会从connect返回, 同时应答一个ACK段; (第三次)

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

断开连接的过程:

如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)

客户端收到FIN, 再返回一个ACK给服务器; (第四次)

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

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

应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;

应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
16天前
|
网络协议 算法 网络性能优化
C语言 网络编程(十五)套接字选项设置
`setsockopt()`函数用于设置套接字选项,如重复使用地址(`SO_REUSEADDR`)、端口(`SO_REUSEPORT`)及超时时间(`SO_RCVTIMEO`)。其参数包括套接字描述符、协议级别、选项名称、选项值及其长度。成功返回0,失败返回-1并设置`errno`。示例展示了如何创建TCP服务器并设置相关选项。配套的`getsockopt()`函数用于获取这些选项的值。
|
28天前
|
存储 Ubuntu Linux
揭开自制NAS的神秘面纱:一步步教你如何用Linux打造专属网络存储王国!
【8月更文挑战第22天】构建Linux NAS系统是技术爱好者的热门项目。通过选择合适的发行版如Alpine Linux或Ubuntu Server,并利用现有硬件,你可以创建一个高效、可定制的存储解决方案。安装Linux后,配置网络设置确保可达性,接着安装Samba或NFS实现文件共享。设置SSH服务方便远程管理,利用`rsync`与`cron`进行定期备份。还可添加Web界面如Nextcloud提升用户体验。这一过程不仅节约成本,还赋予用户高度的灵活性和控制权。随着技术发展,Linux NAS方案持续进化,为用户带来更丰富的功能和可能性。
41 1
|
1月前
|
网络协议 Java
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
这篇文章全面讲解了基于Socket的TCP网络编程,包括Socket基本概念、TCP编程步骤、客户端和服务端的通信过程,并通过具体代码示例展示了客户端与服务端之间的数据通信。同时,还提供了多个案例分析,如客户端发送信息给服务端、客户端发送文件给服务端以及服务端保存文件并返回确认信息给客户端的场景。
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
|
4月前
|
网络协议 算法 网络性能优化
网络编程:TCP/IP与套接字
网络编程:TCP/IP与套接字
|
3月前
|
网络协议 Java API
网络编程套接字(4)——Java套接字(TCP协议)
网络编程套接字(4)——Java套接字(TCP协议)
33 0
|
3月前
|
Java 程序员 Linux
网络编程套接字(3)——Java数据报套接字(UDP协议)
网络编程套接字(3)——Java数据报套接字(UDP协议)
29 0
|
3月前
|
网络协议 API
网络编程套接字(2)——Socket套接字
网络编程套接字(2)——Socket套接字
21 0
|
3月前
网络编程套接字(1)—网络编程基础
网络编程套接字(1)—网络编程基础
23 0
|
2天前
|
人工智能 安全 算法
网络安全与信息安全:构建数字世界的防线
在数字化浪潮席卷全球的今天,网络安全与信息安全已成为维系社会秩序、保障个人隐私与企业机密的关键。本文旨在探讨网络安全漏洞的成因、加密技术的应用及安全意识的提升策略,以期为读者提供一个全面而深入的网络安全知识框架。
|
2天前
|
SQL 安全 网络安全
网络安全与信息安全:保护你的数字世界##
网络安全和信息安全是现代社会中至关重要的话题。随着数字化进程的加速,网络攻击和数据泄露事件频发,给个人、企业乃至国家安全带来了巨大威胁。本文将探讨网络安全漏洞、加密技术以及安全意识等方面的关键知识,帮助你提升对网络安全的认知,保护自己的数字资产。 ##
17 5