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

这篇具有很好参考价值的文章主要介绍了【网络编程】揭开套接字的神秘面纱。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


1 🍑简单理解TCP/UDP协议 🍑

TCP协议:

  • 1️⃣传输层协议
  • 2️⃣有连接
  • 3️⃣可靠传输
  • 4️⃣面向字节流

UDP协议:

  • 1️⃣传输层协议
  • 2️⃣无连接
  • 3️⃣不可靠传输
  • 4️⃣面向数据报

2 🍑网络字节序 🍑

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可.

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

       #include <arpa/inet.h>
       
       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 🍑socket编程接口 🍑

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. 然而, 各种网络协议的地址格式并不相同。
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言
所以当我们使用的时候可以将地址强转成 sockaddr* 类型。

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr *; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

struct sockaddr的定义:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言struct sockaddr_in的定义:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言


4 🍑简单的UDP网络程序 🍑

4.1 🍎基本分析🍎

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

4.2 🍎udpServer.hpp(重点)🍎

#pragma once 
#include<iostream>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include<functional>
using 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"<<endl;
           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"<<endl;
        }

    }
private:
    int _socket;
    uint32_t _port;
    fun_t _service;
};

4.2.1 🍋注意事项🍋

  • 1️⃣ 创建套接字所要的头文件是:
 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>

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

#include <netinet/in.h>
#include <arpa/inet.h>

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

  • 2️⃣由于我们使用的是udp协议,所以我们使用的是SOCK_DGRAM,如果是tcp协议,我们使用的是SOCK_STREAM
    【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言
    至于第三个参数默认给0即可。

  • 3️⃣在bind的时候我们由于类中成员并没有加上IP地址,所以我们使用下面这种写法:
    【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言

4.3 🍎udpClient.cc🍎

#include"udpServer.hpp"

//./udpClient serverIp serverPort

void 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️⃣在客户端这里,我们不难发现我们是没有自己手动bind的,为什么呢?
    在这之前我们先要明确一点,就是客户端也是必须要bind的,这件事只不过是操作系统帮助我们做了。但是大家肯定又有一个疑问:为什么服务端我们要自己手动bind呀?
    server的端口号要我们自己bind是因为服务器的端口号是众所周知的,且不能够随意改变;客户端不需要我们手动bind是因为害怕我们自己bind端口号时会发生冲突,所以这件事就交给了操作系统来帮助我们做。

  • 2️⃣在明确服务端的时候我们使用了下面的接口函数:
    【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言

这个函数有两个作用:

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

与这个函数具有同种功能的函数还有inet_aton
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言
而上面的inet_ntoa则是与inet_aton具有相反的功能。
除此之外,还有inet_ptoninet_ntop:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言在这个系列的转换函数中不仅可以转换IPV4的地址,也可以转换IPV6的地址。

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

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

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地址后仍然不能够正确访问,那么可能是我们云服务器的防火墙没有关,我们进入到我们购买云服务器的官网:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言最后点击确认,就可以了,我们就发现列表中多出了两条:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言
到此为止,我们已经将防火墙给关闭,接下来就进行验证即可:

【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言这样我们就完成了一个简易版本的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;
}

当我们运行时:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言不难发现已经验证成功了。
上述代码中我们简单介绍下popen函数:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言这个函数的主要作用是直接将我们执行的命令重定向到一个文件中。(相比于之前我们还得调用一系列的系统调用方便多了)

当然在客户端和服务端中我们修改代码为生产者消费者模型(具体实现可以让一个线程读取消息,另外一个线程收消息)由于同一个文件描述符可以同时被多个线程读取,所以这样设计是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 🍎验证🍎

【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言我们发现在由一个客户端来通信的时候是没有大问题的,但是我们再加上一个客户端呢?
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言我们发现另外一个客户端发送的消息居然出现问题了,我们发送的消息没有传送到服务器上。
当我们把最先通信的客户端干掉之后:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言消息这才显示到服务端,也就是说当前我们的程序只能够处理一个客户端的情况。究竟是多么逆天的人才能写出这样的程序(doge).我们来想想,究竟是哪里出现了问题。

来看看我们写的代码:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言当有一个客户端获取连接进入处理消息时,那么就糟糕了,因为在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;

        }

【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言这样我们就能够很好的处理了。
除此之外还有一种更为精妙的方式:
【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言我们可以再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,网络,开发语言,C++,套接字,网络编程,C语言
显然此时已经能够成功运行了。除了服务端使用多线程外,客户端也可以用一个线程池来创建,总的来说实现起来这里也不算太难,有兴趣的小伙伴可以参考博主之前实现的【Linux:线程池】来改装一下,有问题可以私信博主。


6 🍑TCP协议通讯流程🍑

【网络编程】揭开套接字的神秘面纱,Linux,网络,开发语言,C++,套接字,网络编程,C语言
这张图大家目前应该是看不太明白的,其实没啥关系,上面讲解的内容在博主后面的文章中会给出详细的解释,这里大家只需要简单的了解下过程就好了。

服务器初始化:

  • 调用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协议层是如何交互的?文章来源地址https://www.toymoban.com/news/detail-643710.html

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段;
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

到了这里,关于【网络编程】揭开套接字的神秘面纱的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【网络编程】网络编程套接字(一)

    端口号(port)是传输层协议的内容. 端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; IP地址 + 端口号能够标识网络上的某一台主机的某一个进程; 一个端口号只能被一个进程占用. 既然端口号是用来标识一个进程,

    2024年02月12日
    浏览(122)
  • 网络编程2(套接字编程)

    套接字编程:如何编写一个网络通信程序 1.网络通信的数据中都会包含一个完整的五元组: sip,sport,dip,dport,protocol(源IP,源端口,对端IP,对端端口,协议) 五元组完整的描述了数据从哪来,到哪去,用什么数据格式 2.网络通信–两个主机进程之间的通信:客户端服务

    2023年04月09日
    浏览(54)
  • 【网络】网络编程套接字(一)

    在前面我们说过可以使用IP地址来标识一台主机,但是我们光有IP地址就可以完成通信了嘛? 答案是:不可以,当我们的主机接收到了数据以后还要确定这个数据是发送给哪一个进程的,两台主机的两个软件进行网络通信时,我们还需要有一个其他的标识来区分出这个数据要给

    2024年02月06日
    浏览(58)
  • 【JavaEE】网络编程之TCP套接字、UDP套接字

    目录 1.网络编程的基本概念 1.1为什么需要网络编程  1.2服务端与用户端 1.3网络编程五元组  1.4套接字的概念 2.UDP套接字编程 2.1UDP套接字的特点  2.2UDP套接字API 2.2.1DatagramSocket类 2.2.2DatagramPacket类  2.2.3基于UDP的回显程序 2.2.4基于UDP的单词查询  3.TCP套接字编程 3.1TCP套接字的特

    2023年04月20日
    浏览(75)
  • 【JaveEE】网络编程之TCP套接字、UDP套接字

    目录 1.网络编程的基本概念 1.1为什么需要网络编程  1.2服务端与用户端 1.3网络编程五元组  1.4套接字的概念 2.UDP套接字编程 2.1UDP套接字的特点  2.2UDP套接字API 2.2.1DatagramSocket类 2.2.2DatagramPacket类  2.2.3基于UDP的回显程序 2.2.4基于UDP的单词查询  3.TCP套接字编程 3.1TCP套接字的特

    2023年04月13日
    浏览(169)
  • 【Linux网络编程】网络编程套接字二

    喜欢的点赞,收藏,关注一下把! TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们

    2024年04月15日
    浏览(64)
  • 网络编程套接字(3)——Java数据报套接字(UDP协议)

    目录 一、Java数据报套接字通信模型 二、UDP数据报套接字编程 1、DatagramSocket         (1)DatagramSocket构造方法         (2)DatagramSocket方法 2、DatagramPacket         (1)DatagramPacket构造方法         (2)DatagramPacket方法 3、InetSocketAddress 三、代码示例:回显服务

    2024年03月12日
    浏览(99)
  • 网络编程【TCP流套接字编程】

    目录 TCP流套接字编程 1.ServerSocket API 2.Socket API 3.TCP中的长短连接 4.回显程序(短连接) 5.服务器和客户端它们的交互过程 6.运行结果及修改代码   ❗❗两个核心: ServerSocket     Socket 1.ServerSocket API ✨ ServerSocket 是创建 TCP服务端Socket的API ServerSocket 构造方法: ServerSocket 方法 :

    2023年04月12日
    浏览(149)
  • UDP网络套接字编程

    先来说说数据在网络上的传输过程吧,我们知道系统其实终究是根据冯诺依曼来构成的,而网络数据是怎么发的呢? 其实很简单,网络有五层。如下: 如上图,我们知道的是,每层对应的操作系统中的那些地方,有些可能说是网络有七层,其实和这个五层一样的。下面我们

    2024年02月04日
    浏览(50)
  • 【网络编程】网络编程套接字(三)TCP网络程序

    与前边的UDP网络程序相同,创建套接字的接口都是socket,下边对socket接口进行介绍: 协议家族选择AF_INET,因为我们要进行网络通信。 而第二个参数,为服务类型,传入SOCK_STREAM,我们编写TCP程序,所以要选择流式的服务。 第三个参数默认传入0,由前两个参数就可以推出这是

    2024年02月16日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包