Linux网络编程:网络编程套接字

news2024/12/23 0:12:52

目录

一. 端口号的概念

二. 对于UDP和TCP协议的认识

三. 网络字节序

3.1 字节序的概念

3.2 网络通信中的字节序

3.3 本地地址格式和网络地址格式 

四. socket编程的常用函数 

4.1 sockaddr结构体

4.2 socket编程常见函数的功能和使用方法

五. UDP协议实现网络通信

5.1 UDP协议服务端的封装

5.2 UDP协议客户端的封装

六. TCP协议实现网络通信

6.1 TCP协议服务端的封装

6.2 TCP协议客户端的封装

七. 总结


一. 端口号的概念

进行网络通信,其根本目的并不是让两个主机之间进行通信,而是让运行在主机上的两个进程之间相互通信。如,我们在日常生活中经常要通过微信发送消息,我发送的消息必须要经过网络传输,才能够被对方接受。我发送的消息,并没有被对方主机的其他应用接受,而只是被对方主机上运行的微信这一进程接受。由此可见,网络通信本质上是网络中的两主机通过网络实现进程间通信,为此,OS必须通过特定的方式,来标识接受数据的进程

端口号,是在某一主机上,用来标识进程唯一性的编号,与之对应的IP地址,用于表示网络中唯一的一台主机,因此,IP地址 + 端口号,可用于表示全网中唯一的一个进程

关于端口号,有如下的基本结论:

  • 端口号用于表示主机中的唯一一个进程。
  • 端口号是一个16位、2字节的整数,其数据类型为uint16_t。
  • IP + 端口号,可用于表示全网唯一一个进程。
  • 一个端口号只能对应一个进程,而一个进程能够对应多个端口号。

端口号(port)和进程pid之间的关系:每个进程都有对应的pid,用于在系统中标识特定进程,但不进行网络通信的进程不需要有端口号,理论上讲id + port,也可以识别网络中唯一一个进程,但是使用端口号和id,能够实现系统进程和网络通信功能之间的解耦。

二. 对于UDP和TCP协议的认识

UDP协议,即用户数据报协议(User Datagram Protocol),其特征有:

  • 属于传输层面的协议。
  • 不需要连接 -- 一方给另一方发送数据时,不需要另一方处于等待状态。
  • 不可靠传输 -- 可能出现失帧、丢包等问题。
  • 面向数据报的通信方式。

TCP协议,即传输控制协议(Transmission Control Protocol),其特征有:

  • 属于控制层面的协议。
  • 需要连接 -- 一方给另一方发送数据时,另一方必须处于等待状态,发送数据前要预先建立连接才能够发送成功。
  • 可靠传输 -- 不会出现失帧、丢包等问题。
  • 面向字节流的通信。

对于需要连接和不需要连接的理解:需要链接,类似于生活中的接打电话,我们给一方打电话的时候,对方需要听到电话铃声,确认接听才能通信,确认接听电话,就类似于网络通信中的建立链接。不需要连接,类似于生活中发送电子邮件的通信方式,我们要给某人发送电子邮件时,可以不用事先通知对方,只要发送,等待对方合适的时候查看即可,对方不需要事先准备接收邮件,即不需要连接。

对于可靠传输和不可靠传输的理解:可靠传输和不可靠传输并不是好坏的评判标准,原因是:(a). UDP协议虽然可能存在丢包失帧等问题,但是发生问题的概率极小,有些时候这并不是不可以接受的。 (b). 虽然TCP协议不会出现UDP这样的不可靠传输的问题,但是可靠通信的建立,是需要成本的,在有些可以一定程度接受数据传输出现问题的场景,采用TCP协议综合效益并不高。

三. 网络字节序

3.1 字节序的概念

内存中存储数据的字节序有两种:(1). 小端字节序 -- 低位存储在低地址,高位存储在高地址。(2). 大端字节序 -- 低位存储在高地址,高位存储在低地址。

图3.1以十六进制表示的数据int num = 0XAABBCCDD为例,展示了大端机和小端机存储数据的规则,这个数据第低位为DD,高位为AA。

图3.1 小端机和大端机使用内存的方式

3.2 网络通信中的字节序

假设这样一种场景,一台小端机要通过网络给一台大端机发送数据,假设他们以他们各自的字节序发送向网络中发数据和从网络中读数据,那么就会出现“乱序”问题,因此需要一定的协议,用于规范网络数据的字节序,以避免“乱序问题”。

规定:网络中的数据,全部采用大端字节序。

我们有时候无法确定发送的数据,或者从网络中读取来的数据是大端还是小端,为了保证发送和读取数据的可靠性,C标准库提供了下面4个函数,可以实现网络和主机数据之间的相互转换:

  • uint32_t htonl(uint32_t hostlong) -- 将32位主机数据转为网络格式。
  • uint16_t htons(uint16_t hostshort) -- 将16位主机数据转为网络格式。
  • uint32_t ntohl(uint32_t netlong) -- 将32位网络数据转为主机格式。
  • uint16_t ntohs(uint16_t netshort) -- 将16位网络数据转为主机格式。 

3.3 本地地址格式和网络地址格式 

一般我们在主机中标识ip地址,都采用const char*数据类型、点分十进制方法来表示, 如1.23.122.234,但是在网络中,为了节省资源,ip应当采用四字节的方法来表示,下面几个函数的功能,是实现本地const char*点分十进制ip格式和网络四字节ip格式之间的转化:

  • in_addr_t inet_addr(const char *cp) -- 本地ip格式转网络ip格式。
  • const char* inet_ntoa(struct in_addr in) -- 网络ip格式转本地ip格式。
  • const char *inet_ntop(int af, const void *src, char *dfs, socklen_t len) -- 网络ip格式转本地ip格式,将转换后的结果存储在dfs所指向的空间中。

注意:一般建议使用inet_ntop函数,而不是采用inet_ntoa函数,因为inet_ntoa为了返回本地格式的ip,会在函数内部开辟一块static空间来记录转换来的结果,这样就带来两个问题:1. 线程不安全  2. 如果多次调用inet_ntoa函数,那么最后一次调用的返回结果会覆盖掉前面的结果。而采用inet_ntop函数,返回结果会被存储在用户指定的buffer空间中,杜绝了inet_ntoa函数的这两个问题。

inet_ntop函数的af参数表示通信方式(AF_INET表示ipv4格式网络地址,AF_INET6表示ipv6格式网络地址),src为指向struct sin_addr类型数据的指针,dfs为接收结果的输出型参数。

四. socket编程的常用函数 

4.1 sockaddr结构体

图4.1给出了sockaddr、sockaddr_in和sockaddr_un的结构,其中sockaddr为socket API抽象出来的一种结构体,使用与ipv4、ipv6、udp、tcp、本地通信,等各种形式的socket。

sockaddr_in为网络通信使用的结构体,sockaddr_un为本地通信使用的结构体。

图4.1 sockaddr结构体

网络通信常用的结构为sockaddr_in,其内容包括:16位地址类型AF_INIT,用于确定通信方式为网络通信、32位IP地址用于在网络中定位特定的主机、8字节填充没有实际意义,一般为0。

struct sockaddr_in的定义:

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)];
  };

struct in_addr的定义:

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

4.2 socket编程常见函数的功能和使用方法

对于socket程序,一般包含四个头文件,即可支持所有的socket相关函数:

  1. #include <sys/types.h> 
  2. #include <sys/socket.h> 
  3. #include <netinet/in.h> 
  4. #include <apra/inet.h>

创建socket文件描述符函数 -- socket:

  • UDP协议和TCP协议、客户端以及服务端,都需要创建socket文件描述符。
  • socket文件描述符与系统级文件描述符的本质相同,都是数组arrayfd对应的一个下标,表示进程打开了某个特定的文件。

socket函数 -- 创建socket文件描述符

函数原型:int socket(int domain, int type, int protocol)

函数参数:

  • domain -- 选取通信的范围(网络还是本地),AF_INET为网络通信,AF_LOCAL为本地通信。
  • type -- 通信类型,UDP协议传SOCK_DGRAM,TCP协议传SOCK_STREAM。
  • protocal -- 指定协议类型,一般传0即可。

返回值:如果成功,返回创建的socket文件描述符,如果失败返回-1。

绑定端口号-- bind:

  • 将用户给出和端口号,在内核中与当前的进程相关联。
  • 在TCP和UDP协议的服务端,都需要使用bind绑定。
  • 在客户端,不采用bind绑定用户设定的端口号,因为如果客户端由用户设置端口号,如果这个端口号被其他进程占用,那么会出现绑定失败的问题。
  • 客户端在第一次给服务端发送消息时,由OS自动分配端口号。

bind函数 -- 绑定端口号

函数原型:int bind(int socket, const struct sockaddr *address, socklen_t *len)

函数参数:

  • socket -- socket文件描述符
  • address -- 输入型参数,用于指定套接字sockaddr
  • len -- 输入型参数,用于给出sockaddr结构体的长度(占用多少字节)

返回值:成功返回0,失败返回-1。

设置监听状态 -- listen:

  • 一般用于TCP协议中的服务器端。
  • 在TCP协议中,只有设置服务器处于监听状态,客户端才可以与服务器建立链接并实现通信。

listen函数 -- 设置监听状态

函数原型:int listen(int socket, int backlog)

函数参数:

  • socket -- socket文件描述符
  • backlog -- 应传一个不太大也不太小的数字。

返回值:成功返回0,失败返回-1。

建立通信连接 -- connect:

  • 一般用于TCP协议的客户端。
  • 在TCP协议中,如果客户端希望与服务端通信,那么就必须与服务端建立连接。
  • 客户端使用connect函数与服务器建立链接,必须保证服务器处于监听状态。

connect函数 -- 建立通信连接

函数原型:int connect(int socket, struct sockaddr *address, socklen_t len)

函数参数:

  • socket -- socket文件描述符
  • address -- 被链接的一端的套接字
  • len -- 套接字长度

返回值:成功返回0,失败返回-1。

接受通信另一方的连接请求 -- accept:

  • 如果另一方尝试与本地进程建立网络通信,那么accept可以用于接收对方的连接。
  • accept函数一般用于TCP协议的服务端,只有accept函数成功接收了另一端的连接请求,才算是真正建立起来通信连接。

accept函数 -- 接受通信另一端的连接请求

函数原型:int accept(int socket, struct sockaddr_in *address, socklen_t *len)

函数参数:

  • socket -- socket文件描述符。
  • address -- 输出型参数,用于获取请求连接的一方的套接字信息。
  • len -- 套接字长度。

返回值:成功返回用于通信的“网络文件”的文件描述符,失败返回-1。

从网络中读取数据函数 -- recvfrom:

  • 从网络中读取指定长度的数据,到指定的缓冲区中。
  • 用于UDP协议的客户端和服务端读取对端消息。

recvfrom函数 -- UDP协议从网络中读取数据

函数原型:ssize_t recvfrom(int socket, void *buffer, size_t length, int flag, struct sockaddr *addr, socklen_t *addr_length)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 接受数据的缓冲区。
  • length -- 至多读取的字节数。
  • flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。
  • addr -- 输出型参数,用于接受发送数据的主机ip及进程端口号。
  • addr_length:套接字长度。

返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。

从网络中读取数据 -- recv:

  • 从网络中读取数据到指定缓冲区。
  • 用于TCP协议中客户端和服务端读取对端消息。

recvfrom函数 -- TCP协议从网络中读取数据

函数原型:ssize_t recv(int socket, void *buffer, size_t length, int flag)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 接受数据的缓冲区。
  • length -- 至多读取的字节数。
  • flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。

返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。

向网络中发送数据函数 -- sendto:

  • 可以通过指定ip和端口号,将数据发送给指定主机的某个进程。
  • 给定端口号和ip时要注意将ip和端口号转为网络格式。

sendto函数 -- 向网络中发送数据

函数原型:ssize_t sendto(int socket, void *buffer, size_t length, int flag, const struct sockaddr *addr, socklen_t addr_length)

函数参数:

  • socket -- socket文件描述符。
  • buffer -- 存储待发送数据的缓冲区。
  • length -- 要发送的数据的字节数。
  • flag -- 发生数据的方式,一般直接设置为0即可。
  • addr -- 指定接受数据的主机ip和端口号。
  • addr_length:套接字长度。

返回值:成功返回发送出去的字节数,失败返回-1。

五. UDP协议实现网络通信

5.1 UDP协议服务端的封装

本文实现一个服务端的demo程序,其功能为:服务端从客户端读取数据,记录数据源主机的ip和端口号,如果源主机第一次向服务器发送数据,就将该主机的ip和端口号插入到哈希表中,每次服务器接受到数据,就将数据发回哈希表中记录的主机,这样就模拟实现了简单的群聊功能。

服务端初始化步骤:创建socket文件描述符 -> 绑定端口号。

服务端启动后的工作流程:通过recvfrom函数从客户端读取数据 -> 判断发送数据的客户端是否已经向服务器发送过数据,如果没有,那么将客户端的ip和端口号插入哈希表 -> 将读取到的数据发回哈希表中记录的客户端。

启动服务端程序时,要指定端口号,以便客户端能够与服务端建立通信,一般来说服务端不用显示设定ip,而是通过INADDR_ANY来设置,这表示无论客户端ip是多少,都可以实现与服务端的通信。

Log.hpp文件(日志打印相关内容):

#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>

#define DEBUG  0
#define NORMAL 1
#define WARING 2
#define ERROR  3
#define FATAL  4

const char* g_levelMap[5] = 
{
    "DEBUG",
    "NORMAL",
    "WARING",
    "ERROR",
    "FATAL"
};

void logMessage(int level, const char *format, ...)
{
    // 1. 输出常规部分
    time_t timeStamp = time(nullptr);
    struct tm *localTime = localtime(&timeStamp);
    printf("[%s]  %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \
                localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);
    
    // 2. 输出用户自定义部分
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
}

udp_serve.hpp文件(对服务端封装):

#pragma once

#include "Log.hpp"
#include <iostream>
#include <string>
#include <unordered_map>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

class Server
{
public:
    // 服务端构造函数
    Server(uint16_t port, const std::string& ip = "")
        : _port(port)
        , _ip(ip)
        , _sock(-1)
    { }

    // 初始化服务器
    void init()
    {
        // 1. 创建网络套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);   
        if(_sock < 0)  // 检查套接字创建成功与否
        {
            logMessage(FATAL, "%d:%s\n", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "套接字创建成功, _sock:%d\n", _sock);

        // 2. bind:将用户设置的ip和port在内核中与进程相关联
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        // INADDR_ANY: 表示服务器在工作过程中可以从任意ip获取数据
        // inet_addr函数: 将主机ip转为4字节网络ip
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());   
        local.sin_port = htons(_port);   // 将主机端口转换为网络端口格式

        if(bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s\n", errno, strerror(errno));
            exit(3);
        }
        logMessage(DEBUG, "用户设置的ip和port在内核中与进程相关联成功!\n");
    }

    // 启动服务器程序
    void start()
    {
        // 服务器进程永不退出
        // 从客户端读取数据
        char buffer[1024];    // 输出缓冲区
        char key[128];         // 存储客户端ip和端口号
        struct sockaddr_in sock_cli;             // 客户端套接字
        memset(&sock_cli, 0, sizeof(sock_cli));  // 初始化0
        socklen_t len = sizeof(sock_cli);        // 输入型参数 -- 套接字长度

        std::string addr_cli;   // 数据源客户端的ip
        uint16_t port_cli;      // 数据源客户端的端口号

        while(true)
        {
            // 输出读取到的数据
            memset(buffer, 0, sizeof(buffer));
            memset(key, 0, sizeof(key));

            ssize_t n = recvfrom(_sock, (void *)buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&sock_cli, &len);

            if(n > 0)
            {
                buffer[n] = 0;   // 添加尾部'/0'
                addr_cli = inet_ntoa(sock_cli.sin_addr);  // inet_ntoa函数负责将网络ip转换为主机ip
                port_cli = ntohs(sock_cli.sin_port);  // 网络套接字转换为主机套接字
                snprintf(key, 128, "[%s-%d]", addr_cli.c_str(), port_cli);
               
                printf("[%s:%d]# %s\n", addr_cli.c_str(), port_cli, buffer);  // 输出发送端的ip和port,以及发送的内容
            }
            else if(n == 0)
            {
                logMessage(DEBUG, "未读取到数据!\n");
                continue;
            }
            else // 数据读取失败
            {
                logMessage(ERROR, "读取数据失败!\n");
                continue;
            }

            // 将客户端的ip和port插入到哈希表
            if(_mp.find(key) == _mp.end())
            {
                _mp.insert({key, sock_cli});
                logMessage(NORMAL, "成功插入客户端, %s\n", key);
            }

            // 将读取到的数据全部发送给客户端主机
            for(const auto& iter : _mp)
            {
                std::string msg_cli;
                msg_cli += key;
                msg_cli += "# ";
                msg_cli += buffer;
                if(sendto(_sock, msg_cli.c_str(), msg_cli.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second)) < 0)
                {
                    logMessage(ERROR, "服务器发回消息失败!");
                    continue;
                } 
                logMessage(NORMAL, "向客户端写回数据 -- %s\n", iter.first.c_str());
            }
        }
    }

    // 析构函数
    ~Server()
    {
        if(_sock >= 0)
        {
            close(_sock);
        }
    }

private:
    uint16_t _port;    // 端口号
    std::string _ip;   // 服务器ip地址
    int _sock;         // 套接字对应文件描述符
    std::unordered_map<std::string, struct sockaddr_in> _mp; // 哈希表,记录接收到信息的客户端的ip和port 
};

udpserve.cc文件(服务端源文件):

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

void usage(const char *command)
{
    std::cout << "\nUsage# " << command << " port\n" << std::endl; 
}

int main(int argc, const char **argv)
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    uint16_t port = static_cast<uint16_t>(atoi(argv[1]));
    std::unique_ptr<Server> psvr(new Server(port));
    psvr->init();
    psvr->start();
    return 0;
}

5.2 UDP协议客户端的封装

本文采用多线程的方式实现UDP客户端demo程序,一个线程负责从服务器读取数据,另一个线程负责向服务器发送数据。

客户端初始化init:创建socket即可,不需要bind端口号,当客户端第一次向服务端发送数据的时候,OS会自动为客户端进程分配端口号。

客户端启动函数执行的工作:创建两个线程,一个调用recvfrom函数从服务端读数据,另一个调用sendto函数向服务器写数据。

启动客户端程序时,需要告知服务器对应的ip和端口号,才能够成功与服务器建立通信。

udp_client.hpp文件(封装客户端):

#pragma once

#include "Log.hpp"
#include "Thread.hpp"
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

struct SendMessageData
{
    int _sock;
    struct sockaddr_in _sock_srv;
    socklen_t _len;

    SendMessageData(int sock, struct sockaddr_in sock_srv)
        : _sock(sock), _sock_srv(sock_srv), _len(sizeof(sock_srv))
    {
    }
};

class Client
{
public:
    // 构造函数
    Client(const std::string &ip, uint16_t port)
        : _ip(ip), _port(port), _sock(-1)
    {
    }

    // 初始化函数
    void init()
    {
        // 1. 创建网络套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. 绑定 -- 客户端需要绑定,但一般不会由程序员来进行绑定
        // 而是在第一次发送消息时,由OS自动分配ip和port
    }

    // 客户端启动程序
    void start()
    {
        // 创建服务器的struct sockaddr
        struct sockaddr_in srv_sock;
        memset(&srv_sock, 0, sizeof(srv_sock));
        srv_sock.sin_family = AF_INET;
        srv_sock.sin_addr.s_addr = inet_addr(_ip.c_str()); // 主机ip转为网络ip
        srv_sock.sin_port = htons(_port);                  // 主机port转为网络port

        SendMessageData sendData(_sock, srv_sock);

        // 发送消息
        thread th_send(send_message, (void *)&sendData);
        th_send.start();

        // 接受反馈回来的消息
        thread th_recieve(recieve_message, (void *)&_sock);
        th_recieve.start();

        th_send.join();
        th_recieve.join();
    }

    // 析构函数
    ~Client()
    {
        if (_sock < 0)
        {
            close(_sock);
        }
    }

private:
    static void *send_message(void *args)
    {
        SendMessageData *ptr = (SendMessageData *)args;
        while (true)
        {
            std::string msg;
            std::cerr << "请输入你要发送的消息: " << std::flush;
            std::getline(std::cin, msg); // 按行读取
            sendto(ptr->_sock, msg.c_str(), msg.size(), 0, (const sockaddr *)&ptr->_sock_srv, ptr->_len);
        }
        return nullptr;
    }

    static void *recieve_message(void *args)
    {
        // memset(buffer, 0, sizeof(buffer));
        char buffer[1024];
        while (true)
        {
            struct sockaddr tmp;
            memset(&tmp, 0, sizeof(tmp));
            socklen_t len = sizeof(tmp);

            ssize_t n = recvfrom(*(int *)args, (void *)buffer, sizeof(buffer) - 1, 0, (sockaddr *)&tmp, &len);
            
            if (n > 0)
            {
                // std::cerr << "aaaa" << std::endl;
                buffer[n] = '\0';
                printf("%s\n", buffer);
            }
        }
        return nullptr;
    }

    std::string _ip; // 发生数据的主机ip
    uint16_t _port;  // 端口号
    int _sock;       // 套接字
};

udp_client.cc文件(客户端源文件):

#include "udp_client.hpp"
#include <memory>
#include <signal.h>

void Usage(const char *command)
{
    std::cout << "/nUsage: " << command << " ip port\n" << std::endl; 
}

int main(int argc, const char **argv)
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    // signal(SIGCHLD, SIG_IGN);

    std::unique_ptr<Client> pcli(new Client(argv[1], atoi(argv[2])));
    pcli->init();
    pcli->start();

    return 0;
}

六. TCP协议实现网络通信

6.1 TCP协议服务端的封装

本文采用多进程的方式来编写服务端demo代码,每次接收到客户端的连接请求,就为这个客户端创建一个进程,用于与该客户端通信。

服务端初始化init:创建socket套接字 -> 将本地ip和端口号在内核中与当前进程绑定 -> 设置服务端进程处于listen状态,以便随时接受客户端的连接请求。

服务端启动start:接受客户端的连接请求并记录请求连接的客户端的套接字 -> 创建子进程 -> 在子进程中调用读取客户端发送的信息 -> 读取成功后,发回给客户端。

启动服务器时,需要显示给出端口号,以便客户端能够顺利连接到服务器。

tcp_server.hpp文件(服务端封装):

#pragma once

#include "Log.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>

const int g_size = 1024;

static void server(int serverSock, struct sockaddr_in sock_cli)
{
    char buffer[g_size];  // 存储读取数据的缓冲区

    while(true)
    {
        ssize_t n = recv(serverSock, buffer, g_size - 1, 0);   // 读取数据
        if(n > 0)
        {
            buffer[n] = '\0';

            uint16_t cli_port = ntohs(sock_cli.sin_port);
            char cli_addr[20]; // 地址
            memset(cli_addr, 0, sizeof(cli_addr));
            // socklen_t len = sizeof(sock_cli.sin_addr);
            inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, sizeof(cli_addr));
            printf("[%s-%d] %s\n", cli_addr, cli_port, buffer);

            // 发回客户端
            send(serverSock, buffer, strlen(buffer), 0);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "对端关闭,读取结束!\n");
            break;
        }
        else  // n < 0
        {
            logMessage(ERROR, "读取失败!\n");
            break;
        }
    }

    close(serverSock);
}

class TcpServer
{
public:
    TcpServer(uint16_t port, const std::string &ip = "")
        : _port(port), _ip(ip), _listenSock(-1)
    { }

    // 服务器初始化
    void init()
    {
        // 1. 创建网络套接字
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0) // 创建socket失败
        {
            logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "socket success, _listenSock:%d\n", _listenSock);

        // 2. 绑定端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;                                                // 设置网络协议族
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 本地格式地址转为网络格式
        local.sin_port = htons(_port);                                             // 本地格式端口号转为网络格式

        if (bind(_listenSock, (const sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s\n", errno, strerror(errno));
            exit(3);
        }
        logMessage(NORMAL, "bind success, %s:%d\n", _ip.c_str(), _port);

        // 3. 设置监听状态
        if (listen(_listenSock, _backlog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s\n", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen success\n");
        logMessage(NORMAL, "Server Init Success!\n");
    }

    // 运行服务器
    void start()
    {
        while (true)
        {
            // 接受客户端的链接请求
            struct sockaddr_in sock_cli;
            memset(&sock_cli, 0, sizeof(sock_cli));
            socklen_t len = sizeof(sock_cli);

            int serverSock = accept(_listenSock, (struct sockaddr *)&sock_cli, &len);
            if (serverSock < 0) // 接受客户端请求失败
            {
                logMessage(ERROR, "accept error, %d:%s\n", errno, strerror(errno));
                continue;
            }

            uint16_t cli_port = ntohs(sock_cli.sin_port);
            char cli_addr[20]; // 地址
            memset(cli_addr, 0, sizeof(cli_addr));
            inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, len);
            cli_addr[strlen(cli_addr)] = '\0';
            logMessage(NORMAL, "accept success [%s-%d]\n", cli_addr, cli_port);

            // 多进程接受客户端信息
            pid_t id = fork();
            if (id == 0)
            {
                if (fork() > 0)
                    exit(0); // 子进程退出

                // 子进程的子进程(孙子进程)此时变为孤儿进程
                // 由1号进程领养,OS自动回收进程
                server(serverSock, sock_cli);
                exit(0);
            }

            waitpid(id, nullptr, 0);
            close(serverSock);
        }
    }

    // 析构函数
    ~TcpServer()
    {
        if (_listenSock >= 0)
        {
            close(_listenSock);
        }
    }

private:
    uint16_t _port;  // 端口号
    std::string _ip; // 本地ip
    int _listenSock; // socket文件描述符

    static const int _backlog = 20;
};

tcp_server.cc文件(服务端源文件):

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

void Usage(const char *proc)
{
    std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}

int main(int argc, const char **argv)
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::unique_ptr<TcpServer> ptr_srv(new TcpServer(atoi(argv[1])));
    ptr_srv->init();
    ptr_srv->start();

    return 0;
}

6.2 TCP协议客户端的封装

TCP协议客户端,需要先调用connect函数,尝试与服务器建立链接,才能与服务器正常通信。

运行TCP协议客户端的时候,需要显示给的IP地址和服务器对应的端口号。

tcp_client.hpp文件(客户端封装):

#pragma once

#include "Log.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>

class TcpClient
{
public:
    TcpClient(const std::string& ip, uint16_t port)
        : _ip(ip), _port(port), _sock(-1)
    { }

    // 初始化客户端
    void init()
    {
        // 创建socket文件描述符
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock < 0)
        {
            logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "socket success, _sock:%d\n", _sock);

        // 客户端与服务端连接
        struct sockaddr_in sock_srv;
        memset(&sock_srv, 0, sizeof(sock_srv));
        sock_srv.sin_family = AF_INET;
        sock_srv.sin_addr.s_addr = inet_addr(_ip.c_str());
        sock_srv.sin_port = htons(_port);
        socklen_t len = sizeof(sock_srv);

        if(connect(_sock, (struct sockaddr *)&sock_srv, len) < 0)
        {
            logMessage(FATAL, "connect error, %d:%s\n", errno, strerror(errno));
            exit(3);
        }

        logMessage(NORMAL, "connect success\n");
        logMessage(NORMAL, "Client Init Success\n");

    }

    void start()
    {
        std::string msg;    // 发送的消息
        char buffer[1024];  // 接受服务器发回的消息

        while(true)
        {
            std::cout << "请输入你要发送的消息: " << std::flush;
            std::getline(std::cin, msg);

            if(msg == "quit")
                break;

            ssize_t n = send(_sock, msg.c_str(), msg.size(), 0);
            if(n > 0)
            {
                logMessage(NORMAL, "成功发送数据!\n");
                ssize_t s = recv(_sock, buffer, 1023, 0);
                if(s > 0)
                {
                    buffer[s] = '\0';
                    printf("回显# %s\n", buffer);
                }
                else if(s == 0)
                {
                    logMessage(DEBUG, "服务器退出!\n");
                    break;
                }
                else
                {
                    logMessage(DEBUG, "获取服务器发回数据失败!\n");
                    continue;
                }
            }
        }
    }

    ~TcpClient()
    {
        if(_sock < 0)
        {
            close(_sock);
        }
    }

private:
    std::string _ip;
    uint16_t _port;
    int _sock;
};

tcp_client.cc文件(客户端源文件):

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

void Usage(const char *proc)
{
    std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}

int main(int argc, const char **argv)
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::unique_ptr<TcpServer> ptr_srv(new TcpServer(atoi(argv[1])));
    ptr_srv->init();
    ptr_srv->start();

    return 0;
}

七. 总结

  • 在网络中,每一台主机都有一个独立的ip地址,每台主机上的进程都可以对应与一个或多个端口号,但是一个端口号不能够对应多个进程。
  • 网络通信,本质上是网络中两台主机上的相应进程之间的进程间通信,通过 IP + Port,可以标识全网中唯一的一个进程。
  • 主机字节序有大端和小端之分,网络数据统一采用大端字节序,进行网络通信时,必须调用相应的接口函数,来实现网络格式和主机格式之间的转换。
  • sockaddr结构体可用于网络(本地)通信,表示套接字,struct sockaddr_in专门用于网络通信,其成员包括网络协议族(AF_INET、AF_INET6、AF_LOCAL等)、主机ip以及端口号,用于socket编程的函数,接受的参数均为struct sockaddr类型。
  • UDP协议全称用户数据报协议,使用UDP协议通信,不需要通信双方建立连接,通信不可靠,可能出现丢包、失帧等问题,但是UDP协议通信的成本较低,因此依旧存在广泛的应用。
  • TCP协议全称传输控制协议,通信双方需要再建立好连接之后才可以通信,通信过程可靠,不会出现丢包等问题,但是可靠通信的成本也相对较高。

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

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

相关文章

c语言练习题60:模拟实现atoi

模拟实现atoi 代码&#xff1a; #include<assert.h> #include<stdio.h> #include<ctype.h> #include<limits.h> int my_atoi(const char* str) {assert(str ! NULL);if (*str \0){return 0;}//isspace 判断一个字符是不是空格while (isspace(*str)){…

视频监控系统/安防监控/视频AI智能分析:小动物识别算法场景汇总

随着人们对生态环境的关注日益提升&#xff0c;大家对动物保护意识也逐渐增强。旭帆科技智能分析网关小动物识别算法应运而生。除了对保护动物的识别以外&#xff0c;旭帆科技AI智能分析网关还可以识别常见的老鼠等动物&#xff0c;助力明厨亮灶监管&#xff0c;保卫食品安全。…

一键集成prometheus监控微服务接口平均响应时长

一、效果展示 二、环境准备 prometheus + grafana环境 参考博文:https://blog.csdn.net/luckywuxn/article/details/129475991 三、导入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter

JAVAEE:采用HTML和JavaScript实现几个基本的页面

1.实现效果&#xff1a; <html><title>学生信息确认</title><body><Form>用户名:<input typetext id"usename"><br>密码: <input typepassword id"userpwd"><br>性别:<input typeradio id"…

华为云云耀云服务器L实例评测|手把手教你搭建MySQL数据库

1. 前言 本文将为读者介绍华为云的云耀云服务器L实例&#xff0c;并提供一份详细的手把手教程&#xff0c;帮助您快速搭建MySQL数据库。 MySQL是一款常用的关系型数据库管理系统&#xff0c;搭建与配置它对于许多业务应用和网站来说都是必需的。本文将以华为云【云耀云服务器L实…

模电2023.9.16

1、放大电路模型 根据功能基本可分为四大类&#xff1a; 电压放大、电流放大、互阻放大和互导放大。 注&#xff1a;互阻放大&#xff1a;电流信号转换为电压信号 互导放大&#xff1a;电压信号转换为电流信号 菱形的电源受控源&#xff0c;圆形的为非受控源 如何区分? 看输入…

【入门篇】ClickHouse 的安装与配置

文章目录 0. 前言ClickHouse的安装1. 添加 ClickHouse 的仓库2. 安装 ClickHouse3. 启动 ClickHouse 服务器4. 使用 ClickHouse 客户端 ClickHouse的配置 1. 详细安装教程1.1. 系统要求1.1. 可用安装包 {#install-from-deb-packages}1.1.1. DEB安装包1.1.1. RPM安装包 {#from-r…

C++中的导入include,头文件,extern,main函数入口及相关编译流程

结论&#xff1a; 1&#xff1a;#include就是复制粘贴 2&#xff1a;C编译的时候&#xff0c;在链接之前&#xff0c;各个文件之间实际上没有联系&#xff0c;只有到了链接的阶段&#xff0c;系统才会到各个cpp文件中去找需要的文件&#xff1b; 一&#xff1a;include的作用…

vue2必备知识点

1、生命周期钩子是如何实现的? 生命周期描述beforeCreatevue实例初始化后&#xff0c;数据观测&#xff08;data observer&#xff09;和事件配置之前。data、computed、watch、methods都无法访问。createdvue实例创建完成后立即调用 &#xff0c;可访问 data、computed、wat…

前端JavaScript中MutationObserver:监测DOM变化的强大工具

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 引言 1. MutationObserver简介 2. MutationObserver的属性 3. MutationObserver的应用场景 3.1 动态内容加载 …

动手学深度学习_个人笔记01_李沐(更新中......)

序言 神经网络——本书中关注的DL模型的前身&#xff0c;被认为是过时的工具。 深度学习在近几年推动了CV、NLP和ASR等领域的快速发展。 关于本书 让DL平易近人&#xff0c;教会概念、背景和代码。 一种结合了代码、数学和HTML的媒介 测试深度学习&#xff08;DL&#xf…

FL Studio21.1.1.3750中文破解百度网盘下载地址含Crack补丁

FL Studio21.1.1.3750中文破解版是最优秀、最繁荣的数字音频工作站 (DAW) 之一&#xff0c;日新月异。它是一款录音机和编辑器&#xff0c;可让您不惜一切代价制作精美的音乐作品并保存精彩的活动画廊。 为方便用户&#xff0c;FL Studio 21提供三种不同的版本——Fruity 版、…

TrOCR——基于transformer模型的OCR手写文字识别

前期我们使用大量的篇幅介绍了手写数字识别与手写文字识别,当然那里主要使用的是CNN卷积神经网络,利用CNN卷积神经网络来训练文字识别的模型。 这里一旦提到OCR相关的技术,肯定第一个想到的便是CNN卷积神经网络,毕竟CNN卷积神经网络在计算机视觉任务上起到了至关重要的作用…

打造“共富果园” 广东乳源推动茶油全产业链高质量发展

新华网广州9月13日电&#xff08;李庆招&#xff09;金秋九月&#xff0c;瓜果飘香&#xff0c;油茶也将迎来采摘期。13日&#xff0c;一场以“中国健康油 茶油新势力”为主题的乳源茶油12221市场体系之产业大会暨供销对接会在广州举行。来自茶油行业的专家、企业家齐聚一堂&am…

Python模块之time中时间戳、时间字符与时间元组之间的相互转换

时间的三种形式 时间戳&#xff0c;根据1970年1月1日00:00:00开始按秒计算的偏移量。 1694868399 时间格式字符串&#xff0c;字符串形式的时间。 2023-09-16 20:46:39 时间元组&#xff08;struct_time&#xff09;&#xff0c;包含9个元素。 time.struct_time(tm_year2023, …

2023 IDC 中国未来企业大奖优秀奖公布,神策数据助力中信建投获“未来运营领军者”优秀奖称号...

今日&#xff0c;全球领先的 IT 市场研究和咨询公司 IDC 正式公布 2023 IDC 中国未来企业大奖优秀奖名单&#xff0c;神策数据的合作客户中信建投证券股份有限公司&#xff08;简称“中信建投”&#xff09;荣获“未来运营领军者”优秀奖称号。 该奖项是 ICT 领域最具权威的奖项…

直线导轨滑块的固定方式

直线导轨滑块是要安装到导轨上的&#xff0c;利用压力使得滑块固定到导轨上&#xff0c;并调整间隙精度&#xff0c;当机械中有振动或冲击力浸染时&#xff0c;滑块和滑轨很有可能发生松动&#xff0c;从而偏离原来的固定地位&#xff0c;影响运行精度与操作寿命&#xff0c;甚…

RADIUS协议基础原理

RADIUS简介 Radius概述Radius架构(c/s模式)Radius特点Radius报文Radius认证报文Radius计费报文Radius授权报文 Radius工作原理 Radius概述 RADIUS&#xff08;Remote Authentication Dial-In User Server&#xff0c;远程认证拨号用户服务&#xff09;是一种分布式的、C/S架构…

Java设计模式-结构性设计模式(外观设计模式)

简介 ⻔⾯模式&#xff0c;隐藏系统的复杂性&#xff0c;并向客户端提供了⼀个客户端可以访问系统的接⼝定义了⼀个⾼层接⼝&#xff0c;这个接⼝使得这系统更加容易使⽤应用场景 xxx是负责消息推送这个⼯作&#xff0c;看起来很轻松&#xff0c;但他们不知道⾥⾯有多复杂&…

Jenkins结合allure生成测试报告

前言&#xff1a; 我们在做自动化测试的过程中最重要的肯定是报告的输出啦&#xff0c;最近几年allure可以说是最最主流报告展示工具啦。 一、服务端安装allure 在安装Jenkins的机器 安装allure&#xff0c;我们在Jenkins上能跑动前提是在对应服务器上代码能正常运行&#xf…