Linux —— Socket编程TCP

目录

[1. EchoServer 将TCP的接口串完](#1. EchoServer 将TCP的接口串完)

[2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连](#2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连)


1. EchoServer 将TCP的接口串完

创建套接字,必须得有一个创建套接字的接口:

socket()

不管是TCP还是UDP底层都需要网络功能,必定使用网络协议:

有些用的是PF_INET,和AF_INET 是等价的:

其实表示的就是IP协议族:

  • type:选用套接字类型,基于流式套接

其实底层用IP,上层用流式套接其实就是TCP套接字。

  • protocol设置为0。
  • 返回值:成功得到一个文件描述符,失败就是-1。

TCP是面向连接的,服务端必须将套接字,设置为listen状态,TCP要随时准备被客户端连接

listen()

  • int backlog:指的是底层将来全连接队列的数值-1,表示的是底层新连接到来时的队列长度
  • 而全连接队列的长度会受到 listen 第二个参数的影响。
  • 全连接队列满了的时候, 就⽆法继续让当前连接的状态进入established 状态了。
  • 这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1。

如果将 backlog 这个参数设置为5,那么底层的全连接数为 6。 backlog 的值就为底层的全连接数-1。backlog 的值一般都是8、16、32。

  • 返回值:成功为0,失败为-1
bash 复制代码
# Makefile
.PHONY:all
all: tcp_client tcp_server

tcp_client:TcpClient.cc
	g++ -o $@ $^ -std=c++17
tcp_server:TcpServer.cc
	g++ -o $@ $^ -std=c++17
	
.PHONY:clean
clean:
	rm -f tcp_client tcp_server
cpp 复制代码
//TcpEchoServer.hpp

#ifndef __TCP_ECHO_SERVER_HPP_
#define __TCP_ECHO_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Logger.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;

class TcpEchoServer
{
public:
    TcpEchoServer(uint16_t port = gport)
        : _sockfd(gdefaultfd), _port(port)
    {
    }

    void Init()
    {
        // 1. 创建套接字文件描述符 create socket fd
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create tcp socket error";
            exit(SOCKET_CREATE_ERR);
        }
        LOG(LogLevel::INFO) << "create tcp socket success: " << _sockfd; // 3 套接字也是文件

        // 2. bind socket fd
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _sockfd;

        // 3. set socket listen 设置套接字为监听状态
        if (listen(_sockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "Listen socket success: " << _sockfd;
    }

    void Start()
    {
        while(true)
        {
            sleep(1);
        }

    }

    ~TcpEchoServer()
    {
    }

private:
    int _sockfd; // 暂时
    uint16_t _port;
};

#endif
cpp 复制代码
//Comm.hpp

#ifndef __COMM_HPP_
#define __COMM_HPP_

#include <iostream>

enum
{
    OK,
    SOCKET_CREATE_ERR,
    SOCKET_BIND_ERR,
    SOCKET_LISTEN_ERR

};

#endif
cpp 复制代码
//TcpServer.cc

#include "TcpEchoServer.hpp"
#include <memory>

int main()
{
    EnableConsoleLogStrategy();
    
    std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>();

    tsvr->Init();
    tsvr->Start();

    return 0;
}
cpp 复制代码
//TcpClient.cc

#include <iostream>

int main()
{
    
}

运行结果:

其实到这里就已经建立了一个TCP服务器了,是卡在了start类中的死循环中。

accept():获取新的连接。没有新连接,accept默认是阻塞的;如果有新连接,accept返回,获取新连接

  • int sockfd:就是刚刚创建、绑定、监听的套接字
  • struct sockaddr *addr:输出型参数,得到对方的套接字
  • socklen_t *addrlen):输入输出型参数,输入表示的是:struct sockaddr in 结构对应的长度,返回的是实际上的套接字结构体的大小
  • 以上的后面的两个参数和UDP中的recvfrom的后面两个参数是一样的,代表的是客户端是谁的问题。TCP是先将套接字的信息发过来。
  • 返回值:成功返回一个文件描述符;失败,返回 -1。

tcp 是会产生更多的fd -- 是的!!返回的一个文件描述符代表的就是一个客户端。

理解 _sockfd vs accept return fd:

例子:

旅游景点,餐厅外基本上都有一个揽客的人,揽到客人之后让客人进入餐厅并叫一个服务员出来服务,服务员在服务客人的时候,刚刚那个揽客的人就有继续出去揽客去了,揽客的那个人只负责获取客源,就是我们刚刚创建的套接字,如今改名为:_listensockfd,而餐厅就是服务器,一桌一桌的客人就是客户端。监听套接字每获取一个新连接,就冲着OS说:我已经获得了一个新连接了,用系统调用accept赶紧获得一个服务员来提供服务,服务员代表的就是accept返回的文件描述符。

所以:

在TCP中,如果你要读写数据,跟揽客的那个sockfd没有关系,而是跟accept的返回值来进行IO读写。

客户端先写一部分,客户端和服务端的面向连接的这个特性先体现出来,客户端不需要显示bind,也不需要 listen 和 accept 获取新连接,但是需要客户端向目标服务器发起连接请求!!!客户端需要做的工作:创建好套接字,直接通过该套接字,向目标服务器发起建立连接的请求。所以说 tcp是面向连接的!!!

connect()

虽然说客户端不需要显示的绑定,但是确实是要进行绑定的!!!那么又在何时绑定的呢???

connect的返回值:连接成功,返回0。连接失败,返回-1。

说明,在发起 connet 的时候,底层是要做 bind 绑定的,当成功调用connect函数的时候,就会绑定本地的套接字,然后向目标主机服务器发起连接请求。所以在connect当中是要传 sockfd 这个套接字,显然,是要将文件信息和网络信息进行绑定。connect后面的两个参数,有点类似 sendto。

sendto 再发消息的时候,后两个参数表示的是要发给谁呀!表示的就是目标主机套接字信息和长度。

cpp 复制代码
//InetAddr.hpp
#pragma once

// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端 -> 先描述再组织

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define Conv(addr) ((struct sockaddr *)&_addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        _addr.sin_port = htons(_port);
    }

public:
    InetAddr(const struct sockaddr_in &addr) : _addr(addr)
    {
        Net2Host();
    }

    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        Host2Net();
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    struct sockaddr *Addr()
    {
        return Conv(_addr);
    }

    socklen_t Length()
    {
        return sizeof(_addr);
    }

    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port); // 同时启动多个客户端
        // return (_ip == addr._ip);                         // 只比较IP的话,好处就是只允许客户端启动一次
    }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr; // 网络风格地址
    // 主机风格地址
    std::string _ip;
    uint16_t _port;
};
cpp 复制代码
//TcpClient.cc

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create client sockfd error" << std::endl;
        exit(SOCKET_CREATE_ERR);
    }

    // tcp客户端,要不要显示的bind???-不要  要不要"bind"???-要
    // 客户端自己的socket地址,让本地OS自主随机选择,尤其是端口号

    // 客户端不需要listen 和 accpet获取新连接

    // 客户端需要向目标服务器发起连接请求!!!

    InetAddr server(serverport,serverip);

    if(connect(sockfd,server.Addr(),server.Length()) != 0)
    {
        std::cerr << "connect server sockfd error" << std::endl;
        exit(SOCKET_CONNECT_ERR);  
        // 这里不一定非得退出,失败后,将创建套接字和建立连接两段代码放在一个while循环中,建立连接失败让它continue,continue之前让它休眠上12秒钟,再次continue
        // 释放老套接字,让他再重新创建新套接字 ,重新在建立再发起连接请求,再失败,在循环,尝试若干次之后,再让客户端退出,此时,就编写了一个客户端断线重连机制
    }

    std::cout << "connect " << server.ToString() << " success" << std::endl;
    
    return 0;
}
cpp 复制代码
//TcpEchoServer.hpp


#ifndef __TCP_ECHO_SERVER_HPP_
#define __TCP_ECHO_SERVER_HPP_

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;

class TcpEchoServer
{
public:
    TcpEchoServer(uint16_t port = gport)
        : _listensockfd(gdefaultfd), _port(port)
    {
    }

    void Init()
    {
        // 1. 创建套接字文件描述符 create socket fd
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create tcp socket error";
            exit(SOCKET_CREATE_ERR);
        }
        LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd; // 3 套接字也是文件

        // 2. bind socket fd
        InetAddr local(_port);
        
        if (bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;

        // 3. set socket listen 设置套接字为监听状态
        // 一个 tcp server ,一旦设置为 listen 状态之后,启动之后,服务器已经算是运行了
        if (listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;
    }

    void Start()
    {
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 给客户提供IO服务,不能用 _listensockfd 来操作
            int sockfd = accept(_listensockfd,(struct sockaddr*)&peer,&len);
            if(sockfd<0)
            {
                LOG(LogLevel::WARNING) << "accept client error";
                continue;
            }
            InetAddr clientaddr(peer);
            LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : "<< sockfd
            << " client addr: " << clientaddr.ToString();
        }

    }

    ~TcpEchoServer()
    {
    }

private:
    int _listensockfd; // 监听socket
    uint16_t _port;
};

#endif

运行结果:

客户端只获取新连接,并没有关闭,所以获得的sockfd是4、5...

服务器端获取了连接,是要进行处理对应的文件描述符进行IO通信。

TCP是面向字节流的,读数据的时候就可以用 read() 系统调用。

返回值:返回的是-1表示的是读取出错了,如果返回值是等于0表示的是读到了文件的结尾,如果在网络中返回值为0,代表的是客户端关闭,

2. 再在TCP的领域中引入多线程、多进程、线程池,将系统和网络相连

cpp 复制代码
//TcpEchoServer.hpp

#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__

#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;

using task_t = std::function<void()>;

class TcpEchoServer
{
private:
    // TCP是面向连接,处理对应的连接时就会出现长连接和短连接的情况
    // 长任务-> sockfd -> 长连接,不适合拿线程池来处理,长连接适合多路转接的技术,打游戏就是长连接的场景,客户端和服务端要进行持续的通信
    // 聊天的场景是一种短连接的场景,前提是用的是TCP的协议
    // 短任务-> 短连接
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while (true)
        {
            buffer[0] = 0;
            // 约定:你给我发过来的是命令字符串!ls -a -l touch XX
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                LOG(LogLevel::DEBUG) << client.ToString() << "say: " << buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client "
                                    << client.ToString() << " quit, me too, close fd: " << sockfd;
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client "
                                       << client.ToString() << " error, sockfd : " << sockfd;
                break;
            }
        }

        close(sockfd); // 一定要关闭
    }

public:
    TcpEchoServer(uint16_t port = gport)
        : _listensockfd(gdefaultfd), _port(port)
    {
    }
    void Init()
    {
        // 1. create socket fd
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create tcp socket error";
            exit(SOCKET_CREATE_ERR);
        }
        LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd; // 3

        // 2. bind socket fd
        InetAddr local(_port);

        if (bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;

        // 3. set socket listen
        // 一个tcp server,listen,启动之后,服务器已经算是运行了
        if (listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;
    }

    class ThreadData
    {
    public:
        ThreadData(int sockfd, TcpEchoServer *self, const InetAddr &addr)
        : _sockfd(sockfd), _self(self), _addr(addr)
        {}
    public:
        int _sockfd;
        TcpEchoServer *_self;
        InetAddr _addr;
    };

    static void *Routine(void *args)
    {
        ThreadData *td = static_cast<ThreadData*>(args);
        pthread_detach(pthread_self());
        td->_self->HandlerIO(td->_sockfd, td->_addr);
        delete td;

        return nullptr;
    }
    void Start()
    {
        // signal(SIGCHLD, SIG_IGN); // 最佳实践
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept client error";
                continue;
            }
            InetAddr clientaddr(peer);
            LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : " << sockfd
                                << " client addr: " << clientaddr.ToString();
            // 多进程,多线程
            // 1. 效率问题,创建进程线程
            // 2. 执行流个数没有上限

            // 进程池4:
            ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, clientaddr](){
                this->HandlerIO(sockfd, clientaddr);
            });

            //version3 多线程做法

            // 多线程是如何看待主线程曾经打开的文件描述符表!共享
            // 主线程和新线程需要关闭历史fd吗?不需要!
            // pthread_t tid;
            // ThreadData *td = new ThreadData(sockfd, this, clientaddr);
            // pthread_create(&tid, nullptr, Routine, (void*)td);

            // handler sockfd
            // version1 -- 单进程的!
            // HandlerIO(sockfd, clientaddr);

            // version2 --- 多进程
            // 创建子进程,子进程是如何看待父进程的fd的?
            // socketfd可以被子进程继承!
            // pid_t id = fork();
            // if (id < 0)
            // {
            //     LOG(LogLevel::FATAL) << "资源不足,创建子进程失败";
            //     exit(FORK_ERR);
            // }
            // else if (id == 0)
            // {
            //     // child -> sockfd -> 也能看到_listensockfd
            //     close(_listensockfd);   //只是改了引用计数
            //     if(fork()>0)
            //         exit(OK);

            //     // 执行任务的是:孙子进程
            //     HandlerIO(sockfd, clientaddr); // while(true)
            //     exit(OK);
            // }
            // else
            // {
            //     close(sockfd); // 1. 关闭无用fd 2. 规避fd泄漏!
            //     // father -> _listensockfd -> sockfd
            //     pid_t rid = waitpid(id, nullptr, 0);
            //     (void)rid;
            // }
        }
    }
    ~TcpEchoServer()
    {
    }

private:
    int _listensockfd; // 监听socket
    uint16_t _port;
};

#endif
cpp 复制代码
//Main.cc

#include "CommandServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

std::string CommandExec(const std::string &commandstr)
{
    return "hello";
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t serverport = std::stoi(argv[1]);

    EnableConsoleLogStrategy();

    std::unique_ptr<CommandServer> tsvr = std::make_unique<CommandServer>(CommandExec,serverport);

    tsvr->Init();
    tsvr->Start();
    return 0;
}

证明网络和客户端之间是打通了的,接着要做的就是执行命令。

popen()函数:将对应的命令传递进来,命令就是一个完整的字符串,直接传递进来,popen就会在底层将命令执行完,如果命令有交互,有输入or输出数据,以特定的文件类型打开,打开成功之后,就会返回一个文件对象。简单来说,popen会将对应的命令执行完,把结果通过类似于文件操作的方式,通过FILE*将命令执行完的结果拿到,拿到之后返回。popen底层做的工作就是创建管道,然后再创建子进程,然后再让子进程程序替换,管道也是文件,将管道文件描述符包装成文件指针,进而返回,上层再通过读文件指针,把文件的内容给读出来了。之后再通过pclose对文件队形进行关闭。

返回值:

cpp 复制代码
//Command.hpp

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <cstdio>

class Command
{
    private:
    bool IsSafe(const std::string &cmd)
    {
        for(auto &c : _command_Whitelist)
        {
            if(cmd == c)
            {
                return true;
            }
        }
        return false;
    }
public:
    Command()
    {
        _command_Whitelist.push_back("ls -a -l");
        _command_Whitelist.push_back("pwd");
        _command_Whitelist.push_back("ll");
        _command_Whitelist.push_back("cat test.txt");
        _command_Whitelist.push_back("touch touch.txt");
        _command_Whitelist.push_back("tree");
        _command_Whitelist.push_back("whoami");
        _command_Whitelist.push_back("who");
    }
    std::string Exec(const std::string &cmd)
    {
        if(!IsSafe(cmd))
        {
            return "坏人";
        }
        std::string result;
        FILE *fp = popen(cmd.c_str(),"r");
        if (fp == NULL)
        {
            result = cmd + " exec error";
        }
        else
        {
            char buffer[1024];
            while(fgets(buffer,sizeof(buffer),fp) !=nullptr )
            {
                result += buffer;
            }
            pclose(fp); 
        }
        return result;
    }
    ~Command(){}
private:
std::vector<std::string> _command_Whitelist;
};

inet_ntoa()接口:将一个4字节的网络地址转换为一个字符串,所对应的返回值是一个char*类型的,实际上返回的是一种字符串风格的IP地址:192.168.1.100,指针返回,字符串在哪里保存?

所以,就不太推荐inet_ntoa()此接口。

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP

地址。但是我们通常用点分⼗进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr(字符串转4字节的)的函数:

cpp 复制代码
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);

in_addr转字符串(4字节转字符串)的函数:

cpp 复制代码
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是

void *addrptr。

关于inet_ntoa

inet_ntoa这个函数返回了⼀个char*, 很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的结果. 那么是否需要调用者⼿动释放呢?

man⼿册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们⼿动进⾏释放。

但是如果我们单进程或者是多线程的去调用以下的代码的时候:

实际的运行结果:

因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区, 这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果。

要解决上面的问题就是:每一个用户调函数的时候传一个自己的缓冲区,不要用公共的缓冲区。

最佳实践:

字符串转in_addr(字符串转4字节的)的函数:int inet_pton(int af, const char *src, void *dst);

  • int af:inet
  • const char *src:要转的字符串,输入型参数,带有const
  • void *dst:输出型参数

主机转网络的时候需要将字符串转为4字节

cpp 复制代码
//InetAddr.hpp

#pragma once

// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端 -> 先描述再组织

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define Conv(addr) ((struct sockaddr *)&_addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        // _ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64]; 
        inet_ntop(AF_INET,&(_addr.sin_addr.s_addr),ipbuffer,sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        // _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr.s_addr));
    }

public:
    InetAddr(const struct sockaddr_in &addr) : _addr(addr)
    {
        Net2Host();
    }

    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        Host2Net();
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    struct sockaddr *Addr()
    {
        return Conv(_addr);
    }

    socklen_t Length()
    {
        return sizeof(_addr);
    }

    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port); // 同时启动多个客户端
        // return (_ip == addr._ip);                         // 只比较IP的话,好处就是只允许客户端启动一次
    }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr; // 网络风格地址
    // 主机风格地址
    std::string _ip;
    uint16_t _port;
};

运行结果:

但是这里是有bug的:

UDP在读写的是不存在问题的,但是TCP是存在问题的,因为TCP是面向字节流的。