【网络】UDP和TCP套接字编程

news2024/11/26 15:31:32

目录

  • 一、初始ip地址和port
  • 二、网络字节序
  • 三、socket编程
    • 1、sockaddr结构
    • 2、socket编程接口
      • 2.1、创建 socket接口
      • 2.2、绑定端口号
      • 2.3、监听socket
      • 2.4、接收请求
      • 2.5、建立连接
      • 2.6、收消息
      • 2.7、发消息
    • 3、UDP套接字编程 -- 现实大小写转换
    • 4、TCP套接字编程 -- 原生多线程现实TCP通信

一、初始ip地址和port

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

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

比如西游记中的唐僧去西天取经,会途径很多王国,而途径王国中,没到一个王国,都会给国王说我们去西天取经,问国王下一站是哪里,这就是查路由表的过程,而上一个王国和要准备去的下一个王国,就表示源Mac地址和目的Mac地址,而去西天就是源IP地址和目的IP地址。

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

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

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

那么,进程PID也是唯一的,为什么不用PID呢?

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

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

二、网络字节序

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • 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);
  • h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如端口号这个字段会以报文的形式发到网络当中的,就要主机转网络字节序,用htons函数,端口号从网络中来的,所以要转成主机序列,用ntohs函数
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

三、socket编程

1、sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6, 然而,各种网络协议的地址格式并不相同
在这里插入图片描述
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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1075701.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter - 底部多选弹框组件

demo 地址: https://github.com/iotjin/jh_flutter_demo 代码不定时更新&#xff0c;请前往github查看最新代码 有时需要弹框选择多个数据&#xff0c;因此写了个底部多选弹框组件 支持搜索&#xff0c;设置默认选中数据&#xff0c;暗黑模式适配 效果图 使用方法 final multiD…

基于FPGA的图像缩小算法实现,包括tb测试文件和MATLAB辅助验证

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 将FPGA的处理结果导出到matlab中显示图像效果&#xff1a; 2.算法运行软件版本 vivado2019.2 matlab2022a 3.部分核心程序 timescale 1ns / 1p…

mysql8压缩包安装

MySQL 8.0 版压缩包安装教程_mysql 压缩包 8.0安装-CSDN博客 1、mysql压缩包 2、参考链接一步一步操作即可。 3、安装&#xff0c;破解navicat. 4、无法连接&#xff0c;参考该链接修改&#xff1a; Mysql 解决1251- Client does not support authentication protocol reques…

易云维®IBMS系统实现了医院楼宇建筑内各专业子系统间的相互操作、快速响应、与联动控制

易云维医院楼宇智能化管理系统&#xff08;IBMS系统&#xff09;可以通过调研医院项目现场情况&#xff0c;了解用户的实际需求&#xff0c;为用户提供合理投资、高效、舒适、方便的环境空间&#xff1b;对医院建筑多个弱电子系统进行集中监控&#xff0c;确保各个弱电子系统安…

【C进阶】字符串函数

C语言中对字符和字符串的处理很频繁&#xff0c;但是C语言本身是没有字符串类型的&#xff0c;字符串通常放在常量字符串中或者字符数组中 字符串常量适用于那些对它不做修改的字符串函数 本章重点介绍处理字符串函数的库函数的使用和注意事项 一、字符串函数 这些函数都要引…

C语言之动态内存管理_柔性数组篇(2)

目录 柔性数组的特点 柔性数组的使用 动态内存函数增容柔性数组模拟实现 柔性数组的优势 今天接着来讲解一下柔性数组知识。 柔性数组的特点 C99中&#xff0c;结构中的最后一个元素允许是未知大小的数组&#xff0c;这就叫做【柔性数组】成员。 结构体中最后一个成员未…

PyCharm搭建Scrapy环境

Scrapy入门 1、Scrapy概述2、PyCharm搭建Scrapy环境3、Scrapy使用四部曲4、Scrapy入门案例4.1、明确目标4.2、制作爬虫4.3、存储数据4.4、运行爬虫 1、Scrapy概述 Scrapy是一个由Python语言开发的适用爬取网站数据、提取结构性数据的Web应用程序框架。主要用于数据挖掘、信息处…

k8spod就绪检查失败

pod 一直未就绪 kube-system metrics-server-7764f6c67c-2kts9 0/1 Running 0 10m kubect describe 查看 就绪探针未通过 Normal Started 3m19s kubelet Started container metrics-server Warning Unhealthy 5s (x20 over 2m55s) kubelet Readiness probe failed: HTTP probe…

Springboot整合Hutool自定义注解实现数据脱敏

一、前言 我们在项目中会处理敏感数据&#xff08;如手机号、身份证号、姓名、地址等&#xff09;时&#xff0c;通常需要对这些数据进行脱敏&#xff0c;以确保数据隐私和安全。 我们本次使用 Hutool 库来轻松实现数据脱敏&#xff0c;如果项目中不让使用&#xff0c;可以自…

各类高危漏洞介绍及验证方式教程(二)

本期整理的漏洞验证教程约包含50多类漏洞&#xff0c;分多个章节编写&#xff0c;可从以下链接获取全文&#xff1a; 各类高危漏洞验证方式.docx (访问密码: 1455) 搭建dvwa测试环境基础教程.docx(访问密码: 1455) web逻辑漏洞挖掘快速入门基础教程.docx (访问密码: 1455) 06 I…

工作杂记-YUV的dump和read

工作小记-YUV的dump和read 工作杂记-YUV的dump和read利用dump生成图片 yuv2imgyuv2img代码 工作杂记-YUV的dump和read 工作中涉及到模型验证相关的工作&#xff0c;这里是三个模型的共同作用&#xff0c;在感知模型读取图片的时候&#xff0c;把输入替换成自己给定的输入&…

Python中如何快速解析JSON对象数组

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 由于浏览器可以迅速地解析JSON对象&#xff0c;它们有助于在客户端和服务器之间传输数据。 本文将描述如何使用Python的JSON模块来传输和接收JSON数据。 JavaSc…

优思学院|揭秘六西格玛:七大迷思你不可不知!

六西格玛的核心理念起源于1970年在摩托罗拉公司诞生。其基本精神一直是持续改进和提升品质&#xff0c;随后在各国呈爆炸性的发展。自2000年开始引进中国后&#xff0c;已经过了约16年的应用。但以2017年的角度回顾中国整体六西格玛的应用广度及熟悉度&#xff0c;发现六西格玛…

【ftp篇】 vsftp(ftp) 每天生成一个动态密码

这里写目录标题 前言为什么需要动态每日生成一个密码&#xff1f;编写脚本定时任务java对应的代码 前言 社长最近接到一个需求&#xff0c;需要ftp每天动态生成一个密码 为什么需要动态每日生成一个密码&#xff1f; 在软硬件通讯过程中&#xff0c;就以共享单车为例&#xff0…

Java解析E文件工具类

import lombok.extern.slf4j.Slf4j;import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List;/*** Description E文件工具类*/ Slf4j public class EFileUtils {/*** 读字符串* param text …

智慧公厕的原理与优势,了解一种更智能的卫生设施

智慧公厕是一种基于现代科技的智能化卫生设施&#xff0c;它的出现给人们的生活带来了巨大的改变和便利。本文将详细介绍智慧公厕的原理和优势&#xff0c;让我们一起了解一种更智能的卫生设施。 智慧公厕的原理主要基于物联网技术。通过将公厕内部各种设备和设施连接到互联网…

Java使用Hutool工具包将汉字转换成汉语拼音

主题&#xff1a;使用Java将汉字转换成拼音 介绍 在Java开发中&#xff0c;有时候我们需要将汉字转换成拼音&#xff0c;以方便进行数据处理、搜索和排序等操作。本文将介绍如何使用Hutool和Pinyin4j这两个Java库来实现汉字转拼音的功能。 依赖库介绍 在开始之前&#xff0c;…

无人直播矩阵系统源码开发------

全自动无人直播系统是一款让商家和企业实现无人直播的系统软件&#xff0c;让商家在门店直播卖货&#xff0c;实现解放双手&#xff0c;无需过多的人工干预。为了满足不同用户的需求&#xff0c;我们推出了OEM功能&#xff0c;让用户可以轻松地将该系统集成到自己的应用程序中。…

软考高项-第九章:项目范围管理

重要知识点&#xff1a; 以上总结&#xff0c;仅供参考。

视频通话中的Camera操作

视频通话也有打开本地摄像头预览的场景&#xff0c;但打开本地Camera预览逻辑&#xff0c;并非在Dailer APP中实现&#xff0c;具体流程图如下。 Dialer app中只调用 1、setCamera用于打开摄像头 相关动作在Ims apk中实现&#xff0c;open函数最后调用了VTSource.java中的doOp…