一、初始ip地址和port

每个主机都有自己的IP地址,IP地址可以表示互联网上唯一的一台主机,而一个报文会携带两种IP地址,一种是IPB,一种是Mac地址,两种IP地址都有自己的源IP地址和目的IP地址,源MAC地址和目的Mac地址。

我们举个例子来看看他们之间的关系:

所以目的IP地址一直不变,而Mac地址会一直变,目的IP就是为报文定制最终目的,路上根据该地址进行路径选择目的Mac,然后根据路径选择的结果来选择下一主机。

那么,两台主机通过数据传输来进行通信,而接受数据的主机有多个进程,这时就需要port(端口号)来定位需要接受数据的进程。

所以IP地址+port则构成进程的唯一性,所以把源IP+源port,目的IP+目的port称为socket通信

用PID也是可以的,但是,我们计算机世界里一套题都各有不同的解法,如果用PID来标识进程的唯一性,不是所有进程都有通信,只有部分进程可能会进行网络通信,但是都用PID来标识,则无法区分,这是其一,最重要的是,如果用PID,而PID是OS层面进程管理的概念,也就是网络模块也要包含进程管理的部分,不然无法认识PID,所以就会增加OS中进程管理和网络管理的耦合度。

注意:一个进程可以绑定多个port,而一个port不能被多个进程绑定

二、网络字节序

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

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

为使网络程序具有可移植性,使同样的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);
  • h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如端口号这个字段会以报文的形式发到网络当中的,就要主机转网络字节序,用htons函数,端口号从网络中来的,所以要转成主机序列,用ntohs函数
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

三、socket编程

1、sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6, 然而,各种网络协议的地址格式并不相同
【网络】UDP和TCP套接字编程-LMLPHP
struct sockaddr_in:是通过网络来通信的sockaddr
struct sockaddr_un:是预间套接字,是通过本地来通信的sockaddr
struct sockaddr:是通用接口,想用网络就传in,用本地就传un,先对16位地址类型进行判断,是AF_INET,就在内部强转成in,是AF_UNIX,就强转成un

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

sockaddr 结构

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in 结构

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

虽然socket API的接口是sockaddr,但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址

in_addr结构

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数

2、socket编程接口

2.1、创建 socket接口

int socket(int domain, int type, int protocol);
  • 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
  • domain:确定是网络通信还是本地通信,AF_INET表示网络通信,是AF_UNIX表示本地通信
  • type:表示将来所对应服务类型是字节流还是数据报,SOCK_STREAM表示是数据报,SOCK_DGRAM表示字节流
  • protocol:确定用那种协议,默认是0,由OS来判断是TCP还是UDP

2.2、绑定端口号

int bind(int socket, const struct sockaddr* address, socklen_t address_len);
  • 绑定端口号 (TCP/UDP, 服务器)
  • 将名称绑定到套接字
  • socket:指定要绑定的套接字的文件描述符。
  • address:指向包含要绑定到套接字的地址的sockaddr结构。
  • address_len:指定地址参数指向的sockaddr结构的长度。

2.3、监听socket

int listen(int socket, int backlog);
  • 开始监听socket (TCP, 服务器)
  • 监听套接字连接并限制传入连接的队列。
  • socket:指定要监听的套接字的文件描述符。
  • backlog:为执行提供了一个提示,实现将使用该提示来限制套接字监听队列中未完成连接的数量。

2.4、接收请求

int accept(int socket, struct sockaddr* address, socklen_t* address_len);
  • 接收请求 (TCP, 服务器)
  • socket:指定一个使用socket()创建的套接字,该套接字已使用bind()绑定到地址,并已成功调用listen()。
  • address:空指针或指向sockaddr结构的指针,其中应返回连接套接字的地址。
  • address_len:指向socklen_t结构,该结构在输入时指定提供的sockaddr结构的长度,在输出时指定存储地址的长度。

2.5、建立连接

int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
  • 建立连接 (TCP, 客户端)
  • sockfd:指定与套接字关联的文件描述符。
  • addr:指向包含对等地址的sockaddr结构。地址的长度和格式取决于套接字的地址族。
  • addrlen:指定地址参数指向的sockaddr结构的长度。

2.6、收消息

ssize_t recvfrom(int socket, void* buffer, size_t length, int flags,
	struct sockaddr* address, socklen_t* address_len);
  • 从套接字接收消息
  • 主要适用于UDP连接,在TCP连接上使用效果并不好。TCP主要用recv()函数收消息。
  • socket:指定套接字文件描述符。
  • buffer:指向应该存储消息的缓冲区。
  • length:指定缓冲区参数指向的缓冲区的长度(以字节为单位)。
  • flags:指定消息接收的类型,读取方式默认为0,以阻塞方式读取
  • address:空指针或指向要存储发送地址的sockaddr结构。地址的长度和格式取决于套接字的地址族。
  • address_len:指定地址参数指向的sockaddr结构的长度。

2.7、发消息

ssize_t sendto(int socket, const void* message, size_t length, int flags,
              const struct sockaddr* dest_addr, socklen_t dest_len);
  • 在套接字上发送消息
  • 主要适用于UDP连接,在TCP连接上使用效果并不好,TCP主要用write()函数发消息。
  • socket:指定套接字文件描述符。
  • message:指向包含要发送的消息的缓冲区。
  • length:以字节为单位指定消息的大小。
  • flags:指定消息传输的类型。
  • dest_addr:指向包含目标地址的sockaddr结构。地址的长度和格式取决于套接字的地址族。
  • dest_len:指定dest_addr参数指向的sockaddr结构的长度。

3、UDP套接字编程 – 现实大小写转换

我们写一个UDP套接字来实现大小写转换
我们先创建一个封装退出码的头文件:err.hpp

#pragma once

enum
{
    USAGE_ERR=1,
    SOCKET_ERR,
    BIND_ERR
};

创建一个UDP服务端的头文件:udp_server.hpp

#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"


namespace ns_server
{

    const static uint16_t default_port = 8080;
    using func_t = std::function<std::string(std::string)>; // 定义了一个函数,参数是string,返回值也是string

    class UdpServer
    {
    public:
        UdpServer(func_t cb, uint16_t port = default_port)
            : service_(cb), port_(port)
        {
            std::cout << "server addr: " << port_ << std::endl;
        }
        void InitServer()
        {
            // 1、创建socket接口,打开网络文件
            sock_ = socket(AF_INET, SOCK_DGRAM, 0); // 这个AF_INET是用来创建socket接口的
            if (sock_ < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR); // 终止进程,0表示成功,非0表示失败
            }
            std::cout << "create socket error: " << sock_ << std::endl; // 3

            // 2、给服务器指明IP地址和port端口号,初始化
            struct sockaddr_in local; // 这个local在哪里定义呢?在用户空间的特点函数的栈帧上,不在内核中
            bzero(&local, sizeof(local));

            local.sin_family = AF_INET;    // 初始化sockaddr_in的结构的,也可以写成PF_INET
            local.sin_port = htons(port_); // 端口号这个字段会以报文的形式发到网络当中的,要主机转网络序列的

            // 1.字符串风格的IP地址,转换成4字节int,1.1.1.1 -> uint32_t -> 能不能强制类型转换?不能,强制是把类型改了,这里要转化
            // 2.需要将主机序列转化成为网络序列
            // inet_addr把上面两件事都做了
            // 3.云服务器,或者一款服务器,一般不需要指明某一个确定的IP
            local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,可以bind本主机上的容易IP.

            if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket error: " << sock_ << std::endl;
        }

        void Start()
        {
            char buffer[1024];
            while (true) // 服务器本质是一个死循环,随时可以使用
            {
                // 收,不断收消息
                struct sockaddr_in peer; // 远端
                socklen_t len = sizeof(peer);  // 这里一定要写清楚,未来你传入的缓冲区大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 从远端把数据传入buffer里
                if (n > 0)
                    buffer[n] = '\0';
                else
                    continue;

                std::cout << "recv done.." << std::endl;

                // 提取client信息
                std::string clientip = inet_ntoa(peer.sin_addr); // 转成字符串风格的ip
                uint16_t clientport = ntohs(peer.sin_port);      // 从网络中来的,所以要转成主机序列
                std::cout << clientip << " - " << clientport << "# " << buffer << std::endl;

                // 做业务处理
                std::string message = service_(buffer); // 收到的一条消息,把消息转给所以人

                // 发,
                // 网络套接字本质也是文件,当向文件中写入时,\0并不需要写到文件中,
                // 因为\0是C语言中的规定,并不是网络的规定,网络对于客户端,也是一样,这就是为什么服务端要加\0的原因
                sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            }
        }
        ~UdpServer()
        {
        }

    private:
        int sock_;
        uint16_t port_;
        func_t service_; // 我们的网络服务器刚刚解决的是网络IO收发的问题,要进行业务处理
    };
}

创建一个UDP服务端的源代码文件,实现大小写转换:udp_server.cc

#include " udp_server.hpp"
#include <memory>
#include <string>
#include <cstdio>
#include "err.hpp"
#include <iostream>
using namespace ns_server;
using namespace std;

static void usage(string proc)
{
    cout << "Usage:\n\t" << proc << "port" << endl; // 服务器必须永远有IP地址
}

// 上层的业务处理,不关心网络发送,只负责信息处理即可
// 上层的业务处理,和下层的网络进行了解耦,把结果再返回
std::string transactionString(std::string request) // request就是一个string
{
    std::string result;
    char c;
    for (auto &r : request)
    {
        if (islower(r))
        {
            c = toupper(r);
            result.push_back(c);
        }
        else
        {
            result.push_back(r);
        }
    }
    return result;
}

//./ubp.server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));

    usvr->InitServer(); // 服务器初始化
    usvr->Start();
    std::cout << "hello server" << std::endl;
    return 0;
}

创建一个UDP客户端的源代码文件:udp_client.cc

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

// 127.0.0.1:本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试

// ./udp_client serverip serverport,客户端启动的时候,必须知道服务端的ip和port
// 客户端并不关心自己的ip和port,只需要别人找到自己就行了
static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << "serverip serverport" << std::endl; // 服务器必须永远有IP地址
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }

    // client 这里要不要bind呢?要的,socket通信的本质[clientip:clientport,server:serverport]
    // client和server在通信时,我们双方地位是对等的,你给我发信息,我也要给你发信息
    // 要不要自己bind呢?自己初始化,自己填一个结构体,然后再来bind,这就是自己bind
    // 不需要自己bind,也不要自己绑定,OS自动给我们进行bind,bind叫系统调用,实际在正常操作时,除了用户直接填一些字段,服务器操作系统本身也是在帮你做工作的
    // 为什么?电脑上,手机上有不同的客户端,这些客户端来自不同的企业,比如,抖音或淘宝等
    // 所以,client的port要随机让OS分配,防止client出现启动冲突
    // 为什么server要自己bind?
    // 1、server的端口不能随意改变,众所周知且不能随意改变
    // 2、服务器都是一家公司的,只要同一家公司,端口号需要统一规范化

    // 明确server是谁
    struct 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()); // 服务端不用明确ip,但是客户端要明确server对应的ip地址

    //未来客户端不发数据,也有收数据的权利,也要改成多线程,下节课写
    while (true)
    {
        // 先有消息,用户输入,比如平时刷抖音,点的赞等等
        std::string message;
        std::cout << "[我的服务器]#";
        // std::cin >> message;
        std::getline(std::cin,message);

        // 发
        // 什么时候bind
        // 客户端也要有ip和port,所以一定要绑定,那么什么时候绑定呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP
        // 1.bind 2.构建发送的数据报文
        //  message.c_str是发送的数据,作为的客户端,发给谁,发给服务端
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));

        // 收,接受
        char buffer[2048];
        struct sockaddr_in temp; // 客户端也会收到别人的消息,今天就一个服务器,给服务器发消息,一定会收到服务器转回来的消息,默认temp中的字段是server
        // 未来你的客户端也会和别的客户端通信,或者别的服务器通信,可能收消息,来自不同的服务器,所以直接填充&temp
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout <<  buffer << std::endl;
        }
    }

    return 0;
}

4、TCP套接字编程 – 原生多线程现实TCP通信

我们先创建一个封装退出码的头文件:err.hpp

#pragma once

enum
{
    USAGE_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

创建一个TCP服务端的头文件:tcp_server.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include "err.hpp"

namespace ns_server
{
    static const uint16_t defaultport = 8081;
    static const int backlog = 32; 

    using func_t = std::function<std::string(const std::string &)>;

    class TcpServer;//声明
    class ThreadData
    {
    public:
        ThreadData(int fd, const std::string &ip, const uint16_t &port,TcpServer *ts)
            : sock(fd), clientip(ip), clientport(port),current(ts)
        {
        }

    public:
        int sock;
        std::string clientip;
        uint16_t clientport;
        TcpServer* current;//声明后,初始化
    };

    class TcpServer
    {
    public:
        TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true)
        {
        }
        void initServer()
        {
            // 1.创建socket ,文件
            listensock_ = socket(AF_INET, SOCK_STREAM, 0);//创建成功,是3号文件描述符
            if (listensock_ < 0)
            {
                std::cerr << "create socket error" << std::endl;
                exit(SOCKET_ERR);
            }

            // 2.绑定
            struct 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; // 是全0,也可以写成htonl(INADDR_ANY)
            if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error " << std::endl;
                exit(BIND_ERR);
            }

            // 3.监听
            if (listen(listensock_, backlog) < 0)
            {
                std::cerr << "listen socket error " << std::endl;
                exit(LISTEN_ERR);
            }
        }
        void start()
        {
            // signal(SIGCHLD,SIG_IGN);//对信号进行忽略,忽略后,就不需要等了,不用写waitpid,推荐

            // signal(SIGCHLD,handler);//以回收的方式,不推进

            quit_ = false;//这个服务器不quit就一直运行
            while (true)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                // 4.获取连接,accept
                int sock = accept(listensock_, (struct sockaddr *)&client, &len);
                if (sock < 0)
                {
                    std::cout << "accept error" << std::endl;
                    continue;
                }

                // 提取client信息
                std::string clientip = inet_ntoa(client.sin_addr); // client的ip
                uint16_t clientport = ntohs(client.sin_port);      // 从网络中来的,所以要转出网络序列,client的port

                // 5.获取新连接成功,就开始进行业务处理
                std::cout << "获取新连接成功: " << sock << " from " << listensock_ << " , "
                          << clientip << " - " << clientport << std::endl; // 文件描述符sock from listensock,打印出来

                // v3版本,多线程--原生多线程
                // 1.要不要关闭不要的socket?多线程中,文件描述符表都是共享的,不需要,直接可以被外部所看到,关了,会影响我服务器
                // 2.要不要回收线程?如何回收,会不会阻塞?要回收,可以分离,分离后,主线程不用关闭新线程了,主线程可以回过去继续获取连接
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, clientip, clientport,this);
                pthread_create(&tid, nullptr, threadRoutine, td);
            }
        }

        static void *threadRoutine(void *args)
        {
            pthread_detach(pthread_self());//分离后,主线程不用关闭新线程了,主线程可以回过去继续获取连接

            ThreadData *td = static_cast<ThreadData *>(args);
            td->current->service(td->sock,td->clientip,td->clientport);
            delete td;
        }

        void service(int sock, const std::string &clientip, const uint16_t &clientport)
        {
            std::string who = clientip + "-" + std::to_string(clientport);
            char buffer[1024];
            while (true)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // 读数据,大于0,读取成功
                if (s > 0)
                {
                    buffer[s] = 0;
                    std::string res = func_(buffer); // 读到数据以后,回调一下,回调出去,再回调回来,把结果给我,就可以进行特定的处理
                    std::cout << who << ">>> " << res << std::endl;

                    write(sock, res.c_str(), res.size()); // 对对方的客户端进行写回的操作
                }
                else if (s == 0)
                {
                    // 对方把连接关闭了
                    close(sock);
                    std::cout << who << " quit,me too " << std::endl;
                    break;
                }
                else
                {
                    close(sock);
                    std::cout << "read error: " << strerror(errno) << std::endl;
                    break;
                }
            }
        }
        ~TcpServer()
        {
        }

    private:
        uint16_t port_;
        int listensock_;

        bool quit_; // 表示服务器是否启动
        func_t func_;
    };
}

创建一个TCP服务端的源代码文件:tcp_server.cc

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

using namespace std;
using namespace ns_server;

static void usage(string proc)
{
    std::cout << "Usage:\n\t" << proc << "port\n"
              << std::endl; // 服务器必须永远有IP地址
}

std::string echo(const std::string &message)
{
    return message;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));

    tsvr->initServer();
    tsvr->start();
    return 0;
}

创建一个TCP客户端的源代码文件:tcp_client.cc

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

using namespace std;

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << "serverip serverport\n"
              << std::endl; // 服务器必须永远有IP地址
}
int main(int argc, char *argv[])
{
    // 准备工作
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }

    // 客户端要不要bind?要
    // 要不要自己bind?不要,因为client要让OS自动给用户进行bind
    // 要不要listen?不要,因为客户端永远都是连别人的,永远都是别人处于listen状态
    // 要不要获取连接,不需要,获取连接都是服务器做的事情
    // 那么客户端需要什么

    // 2.发起连接
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);

    //serverip.c_str()这是字符串风格的ip,转成&server.sin_addr 4字节的ip地址
    inet_aton(serverip.c_str(), &server.sin_addr); // 不能是INADDR_ANY,因为客户端要知道服务器的ip,是字符串风格的点分十进制的ip地址,要转成4字节

    int cnt = 5;
    while (connect(sock, (struct sockaddr *)&server, sizeof(server)) != 0) // 不等于0,失败,sock向server发消息
    {
        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;
}
10-10 10:29