Bootstrap

网络编程套接字

一、源IP地址和目的IP地址

  • ip地址的概念参见网络基础知识
  • 源IP地址指的是数据包的来源地址,即发送数据包的设备的IP地址。它是数据包的起点,用于在网络中唯一标识发送数据包的设备。
  • 目的IP地址指的是数据包的目的地址,即数据包要传递到的设备的IP地址。它是数据包的终点,用于在网络中唯一标识接收数据包的设备,即数据包应该被发送到哪个设备。

二、端口号

1、介绍

  • 端口号是通过端口来标记的,它是16位整数,范围从0到65535。每个端口号都对应着一种特定的服务或应用程序,是客户端与服务器进行网络通信时的重要标识。
  • IP地址能表示网络中唯一的一台主机,而端口号用来标识该主机上的唯一一个进程。所以,IP地址和端口号组合在一起可以标识全网唯一的一个进程。所以,网络通信的本质也是进程间通信。
  • 一个进程可以绑定多个端口号,但一个端口号不可以被多个进程绑定。

2、与进程pid的关系

  • 虽然进程pid能够标识一台主机上进程的唯一性,但不是所有的进程都要进行网络通信,而所有进程都要有进程pid。
  • 不用进程pid而用端口号可以实现系统和网络功能的解耦。

3、作用

  • 标识服务或应用程序:端口号用于标识网络中的特定服务或应用程序,确保数据包能够准确地传递到目标程序。
  • 区分不同服务:在同一台计算机上,不同的服务或应用程序需要使用不同的端口号,以便计算机能够正确地将数据包传输到目标程序。
  • 网络通信:端口号与IP地址配合,确保数据能够准确传递到网络中的特定服务或应用程序,从而实现网络通信。

4、分类

  • 常用端口(熟知端口):端口号小于256的端口被定义为常用端口,这些端口通常由系统或应用程序保留,用于提供常见的网络服务。例如,HTTP服务默认使用端口号80,HTTPS服务默认使用端口号443等。
  • 注册端口:端口号从1024到49151的端口被称为注册端口,这些端口被IANA(互联网号码分配机构)指定为特殊服务使用。虽然这些端口不是系统保留的,但它们在特定应用程序或协议中通常具有特定的含义。
  • 动态端口(临时端口):端口号大于49151(或5000,具体取决于TCP/IP实现)的端口被称为动态端口或临时端口。这些端口通常由客户端在需要时动态分配,用于建立临时的网络连接。
  • 其中,0到1023是系统保留的端口号,通常用于提供常见的网络服务。1024到65535是用户可用的端口号范围。

三、网络字节序

1、介绍

  • 由于不同的计算机体系结构(如x86、ARM等)和操作系统可能采用不同的字节序,因此在网络通信中必须统一字节序以确保数据的正确传输和解析。
  • 网络字节序(Network Byte Order)的引入正是为了解决这一问题,它是一种规范,用于在计算机网络中进行数据通信时统一数据的字节顺序。
  • 网络字节序规定了在网络通信中使用大端字节序(Big Endian)作为标准。在大端字节序中,数据的高位字节存储在低地址处,低位字节存储在高地址处。这种排序方式确保了数据在不同主机之间传输时能够被正确解释,因为网络字节序与具体的CPU类型、操作系统等无关。这也保证了数据在不同主机之间的兼容性。

2、字节序转换函数

(1)函数

在这里插入图片描述

(2)说明

  • htonl (htons)函数将无符号(短)整数hostlong (hostshort)从主机字节顺序转换为网络字节顺序。
  • ntohl(ntohs)函数将无符号(短)整数netlong(netshort)从网络字节顺序转换为主机字节顺序。

四、套接字socket

1、介绍

  • 套接字是一种抽象的数据结构,用于在网络应用程序之间提供通信接口。它可以看作是一个端点,用于发送和接收数据,使得运行在不同机器上的应用程序能够交换信息,从而实现网络功能。
  • 套接字的概念最早由Unix系统的开发者引入,也有说法认为是由BSD(伯克利软件套件)在1982年引入的。套接字API最初是为了提供一个统一的接口,以便程序员可以轻松地编写网络应用程序,而无需深入理解网络协议的复杂性。
  • 套接字工作在网络的传输层,它利用了TCP/IP协议族中的TCP或UDP协议来传送数据。TCP套接字保证了数据的有序和可靠传输,而UDP套接字则提供了无连接的服务,它不保证数据包的到达顺序或所有包都能到达。

2、类型

  • 套接字根据其数据传输方式的不同,可以分为三种类型。
  • 流式套接字(SOCK_STREAM):基于TCP协议,提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。
  • 数据报套接字(SOCK_DGRAM):基于UDP协议,提供无连接的数据传输服务。该服务不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
  • 原始套接字(SOCK_RAW):可以读写内核没有处理的IP数据包,而流式套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

3、套接字编程

  • 套接字编程是一种网络编程技术,允许应用程序通过网络发送和接收数据。
  • 在套接字编程中,程序员需要创建套接字、绑定地址和端口、监听连接请求(对于服务器)、建立连接(对于客户端)、发送和接收数据等。这些操作通常通过调用操作系统提供的套接字API来完成。

五、socket编程接口

1、socket

(1)介绍

  • socket为通信创建一个端点,并返回一个引用该端点的文件描述符。即创建一个新的socket(套接字)。
  • 成功调用返回的文件描述符将是该进程当前未打开的文件描述符中编号最低的那个;失败则返回-1,并设置errno。

(2)函数

在这里插入图片描述

(3)domain参数

  • 指定通信域,该参数的选择是将用于通信的协议族。
  • AF_UNIX:用于本地通信。
  • AF_INET:IPv4互联网协议。
  • AF_INET6:IPv6互联网协议。

(4)type参数

  • 套接字具有指定的类型type,该类型(type)指定了通信语义。
  • SOCK_STREAM:提供有序、可靠、双向、基于连接的字节流,可以支持带外数据传输机制。
  • SOCK_DGRAM:支持数据报(固定最大长度的无连接、不可靠的消息)。

(5)protocol参数

  • protocol参数指定了与套接字一起使用的特定协议。通常,在给定的协议族中,只有一个协议支持特定的套接字类型,在这种情况下,协议可以指定为0。
  • 然而,可能存在有许多协议的情况,这时就必须以这种方式(protocol参数)指定特定的协议。要使用的协议号特定于要进行通信的通信域,即相当于替换。

2、bind

(1)介绍

  • 当使用socket创建套接字后,此时它存在于名称空间(地址族)中,但没有分配地址。
  • bind函数将addr指定的地址分配给文件描述符sockfd引用的套接字。addrlen指定addr指向的地址结构的大小(以字节为单位)。传统上,此操作称为为套接字分配名称。即将socket绑定到一个地址(IP地址和端口号)。
  • 在SOCK_STREAM套接字可以接收连接之前,通常需要使用bind分配一个本地地址。

(2)函数

在这里插入图片描述

3、listen

(1)介绍

  • listen将sockfd引用的套接字标记为被动套接字,即使用accept接受传入连接请求的套接字。即使对应socket(sockfd)处于监听状态。
  • sockfd参数是一个文件描述符,它引用SOCK_STREAM或SOCK_SEQPACKET类型的套接字,即socket函数返回值。
  • backlog参数定义了sockfd的挂起连接队列可能增长的最大长度。如果连接请求在队列已满时到达,客户端可能会收到一个指示为ECONNREFUSED的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后在连接时重新尝试。

(2)函数

在这里插入图片描述

4、accept

(1)介绍

  • accept用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。它提取监听套接字sockfd(参数)的挂起连接队列上的第一个连接请求,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。
  • 新创建的套接字未处于侦听状态,即接受一个连接请求。原始套接字(参数)sockfd不受此调用的影响。

(2)参数

  • sockfd:一个使用socket创建、通过bind绑定到本地地址,并在listen后监听连接的套接字。
  • addr:指向sockaddr结构的指针。如通信层所知,此结构填充了对等套接字的地址。返回地址addr的确切格式由套接字的地址族决定。当addr为NULL时,不填写任何内容;在这种情况下,不使用addrlen,它也应该是NULL。
  • addrlen:一个value-result参数,调用者必须对其进行初始化,以包含addr指向的结构的大小(以字节为单位);返回时,它将包含对等地址的实际大小。如果提供的缓冲区太小,返回的地址将被截断;在这种情况下,addrlen将返回一个大于提供给调用的值。
  • 如果队列中不存在挂起的连接,并且套接字未标记为非阻塞,则accept会阻塞住,直到存在连接。如果套接字标记为非阻塞,并且队列上不存在挂起的连接,则accept失败,并返回错误EAGAIN或EWOULDBLOCK。

(3)函数

在这里插入图片描述

5、connect

(1)介绍

  • connect用于建立链接,通常用于客户端进行连接服务器的操作。
  • connect将文件描述符sockfd引用的套接字连接到addr指定的地址。addrlen参数指定addr的大小。addr中地址的格式由sockfd套接字的地址空间决定。
  • 如果套接字sockfd的类型为SOCK_DGRAM,则addr是默认情况下发送数据报的地址,也是接收数据报的唯一地址。
  • 如果套接字的类型为SOCK_STREAM或SOCK_SEQPACKET,则此调用将尝试连接到绑定到addr指定地址的套接字。

(2)函数

在这里插入图片描述

六、sockaddr结构

1、sockaddr

  • sockaddr是一个通用的套接字地址结构体,用于在网络编程中表示套接字的地址信息。
  • sockaddr结构体一般被用作函数参数,比如在socket编程中调用bind函数时,需要传入一个指向sockaddr结构体的指针作为参数,以指定套接字的地址信息。
    在这里插入图片描述

2、sockaddr_in

  • sockaddr_in是sockaddr结构体的一个变体,专门用于表示IPv4地址和端口号。
  • 通过sockaddr_in结构体,可以方便地表示和操作IPv4地址和端口号。在实际网络编程中,可以使用该结构体来指定套接字的本地或远程地址,进行网络通信。
    在这里插入图片描述
    在这里插入图片描述

3、sockaddr_un

  • sockaddr_un是另一个sockaddr结构体的变体,用于表示Unix域协议(IPC)的地址。它通常用于在同一台计算机上的不同进程之间进行通信。
  • sockaddr_un结构体适用于本机通信的场景。在同一台计算机上,不同的进程如果需要进行通信,就可以通过sockaddr_un来实现。它允许进程通过UNIX文件系统维护的虚拟数据传输域进行通信,而无需通过计算机网络。
    在这里插入图片描述

4、示意图

在这里插入图片描述

七、地址转换函数

1、inet_aton

(1)介绍

  • inet_aton将互联网主机地址cp从IPv4 numbers-and-dots表示法转换为二进制形式(按网络字节顺序),并将其存储在inp指向的结构中。
  • 如果地址有效,inet_anton返回1,即成功解释,否则返回零。

(2)函数

在这里插入图片描述

2、inet_pton

(1)介绍

  • inet_pton是一个通用的函数,支持IPv4和IPv6地址的转换。它可以将点分十进制或冒分十六进制字符串表示的IP地址转换为网络字节序的二进制形式。

(2)参数

  • af:地址族,对于IPv4使用AF_INET,对于IPv6使用AF_INET6。
  • src:输入字符串表示的IP地址。
  • dst:输出二进制表示的IP地址。
  • 返回值:如果转换成功,返回1;如果输入的字符串不是有效的IP地址,返回0;如果发生错误,返回-1并将errno设置为EAFNOSUPPORT。

(3)函数

在这里插入图片描述

(4)示例

struct in_addr ipv4Address;  
const char *ipString = "192.168.1.1";  
inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));

3、inet_ntoa

(1)介绍

  • inet_ntoa函数将以网络字节顺序给出的互联网主机地址in转换为IPv4点分十进制表示法的字符串。
  • 返回值返回的是指向静态缓冲区的指针,该缓冲区保存有由参数in的值转换而来的点分十进制表示的IP地址字符串。如果函数调用失败,则返回一个空指针NULL。
  • 需要注意的是,由于返回的是指向静态缓冲区的指针,因此不宜多次调用,即后续调用将覆盖该缓冲区,因此,inet_ntoa不是线程安全的函数。

(2)函数

在这里插入图片描述

4、inet_ntop

(1)介绍

  • 此函数将af地址族中的网络地址结构src转换为字符串。生成的字符串被复制到dst指向的缓冲区,该缓冲区必须是非空指针。调用者在参数size中指定此缓冲区中可用的字节数。
  • 成功后,inet_ntop返回一个指向dst的非空指针。如果发生错误,则返回NULL,并设置errno表示错误。

(2)函数

在这里插入图片描述

八、示例

1、客服端(client)

(1)代码

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "Log.hpp"

void usage(const char *process)
{
    std::cout << "usage: \n\t";
    std::cout << process << " server_ip server_port" << std::endl;
}

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

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 1;
    }
    int n = connect(fd, (const sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "client, connet error" /*, reconnect: " << cnt*/ << std::endl;
        return 1;
    }

    std::string message;
    char receive[4096]; // 大小可设置大一些

    while (true)
    {
        std::cout << "enter your message:";
        std::getline(std::cin, message);
        ssize_t sz = write(fd, message.c_str(), message.size());
        if (sz < 0)
        {
            std::cerr << "write error" << std::endl;
        }
        sz = read(fd, receive, sizeof(receive));
        if (sz > 0)
        {
            receive[sz] = 0;
            std::cout << receive << std::endl;
        }
        else if (sz == 0)
        {
            std::cerr << "server[" << server_ip << ":" << server_port << "] quit" << std::endl;
        }
        else
        {
            std::cerr << "read error" << std::endl;
        }
    }
    close(fd);

    return 0;
}

(2)说明

  • 此处为客户端,所以其需要用户给予服务端的IP地址和端口号,加上运行程序,就需要为该程序传入三个参数。接着就是对IP地址和端口号进行处理,方便后续的操作。
  • 等IP地址和端口号处理完就可以进行后续操作,即创建套接字和进行连接。
  • 此处进行操作所使用的是TCP协议,所以用的是SOCK_STREAM,在上方的socket说明中已提及。
  • 因为此处的代码实现的是单进程的,所以只需进行一次链接操作,接着就是进行正常通信。否则,程序运行时,客户端链接一次就只能发送一次数据,如果要想继续通信则需要再次链接,这会变得比较麻烦。
  • 由于客户端不需要固定的端口号,因此此处代码不必调用bind,客户端的端口号由内核自动分配。但客户端不是不允许调用bind。只是没有必要调用bind固定一个端口号,否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。

2、服务端(server)

(1)主函数代码

#include <iostream>
#include <memory>
#include "TcpServer.hpp"

void usage(const char *process)
{
    std::cout << "usage: \n\t";
    std::cout << process << " port[1024+]\n"
              << std::endl; // 输出内容最后可加一个\n换行,这样更美观
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> up(new TcpServer(port));
    up->Initial();
    up->Start();

    return 0;
}

(2)说明

  • 此处为服务端主函数的代码,所以其需要用户给予服务端的端口号,加上运行程序,就需要为该程序传入两个参数。接着就是对端口号进行处理。
  • 然后就是创建服务的类,用unique_ptr管理,这样就不需要用户管理释放问题。接着就是调用对应函数进行初始化和正常操作。

(3)服务端代码

#pragma once

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

int backlog = 10;

enum
{
    SOCK_ERROR = 1,
    BIND_ERROR,
    Listen_ERROR,
    ACCEPT_ERROR,
};

Log lg;

class TcpServer
{
public:
    TcpServer(uint16_t port, const std::string &ip = "0.0.0.0", int fd = -1)
        : ip_(ip), port_(port), listenfd_(fd)
    {
    }

    void Initial()
    {
        listenfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd_ < 0)
        {
            lg(Fatal, "creat sockfd error, errno: %d, error string: %s", errno, strerror(errno));
            exit(SOCK_ERROR);
        }
        lg(Info, "creat sockfd success, listenfd: %d", listenfd_);

        sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &local.sin_addr);

        if (bind(listenfd_, (const sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, error string: %s", errno, strerror(errno));
            exit(BIND_ERROR);
        }
        lg(Info, "bind success, listenfd_: %d", listenfd_);

        if (listen(listenfd_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, error string: %s", errno, strerror(errno));
            exit(Listen_ERROR);
        }
        lg(Info, "listen success, listenfd_: %d", listenfd_);
    }

    void Start()
    {
        for (;;) // 需要使用循环,否则client端退出后,本程序也会退出
        {
            sockaddr_in client;
            socklen_t len = sizeof(client);
            int fd = accept(listenfd_, (sockaddr *)&client, &len);
            if (fd < 0)
            {
                lg(Fatal, "accept error, errno: %d, error string: %s", errno, strerror(errno));
                exit(ACCEPT_ERROR);
            }

            char client_ip[32];
            inet_ntop(AF_INET, &client.sin_addr, client_ip, sizeof(client_ip)); // 是计数client_ip的大小而不是client
            uint16_t client_port = ntohs(client.sin_port); // 转换为主机序列

            // 输出消息,提示链接成功
            lg(Info, "get a new link..., fd: %d, client_ip: %s, client_port: %d", fd, client_ip, client_port);

            // 单进程
            close(listenfd_);
            Server(fd, client_ip, client_port);
        }
    }

    void Server(int fd, const std::string &client_ip, uint16_t client_port)
    {
        char buff[1024];
        while (true)
        {
            ssize_t sz = read(fd, buff, sizeof(buff));
            if (sz > 0)
            {
                buff[sz] = 0;
                std::cout << "client say: " << buff << std::endl;
                std::string res = "server say: ";
                res += buff;
                write(fd, res.c_str(), res.size());
            }
            else if (sz == 0)
            {
                lg(Info, "%s:%d quit, server close sockfd: %d", client_ip.c_str(), client_port, fd);
                break;
            }
            else
            {
                lg(Warning, "read error, errno: %d, error string: %s", errno, strerror(errno));
                break;
            }
        }
        close(fd);
    }

private:
    int listenfd_;
    int port_;
    std::string ip_;
};

(4)说明

  • 因为运行该代码的环境是同一台机器,所以IP地址在初始化时就默认为全零。如果需要指定地址,则需要在上方的主函数代码中添加一个接收参数并传入本代码中。
  • Initial初始化函数中,需要进行创建套接字、绑定和监听操作。Start正常运行函数则需要用accept接收来自客户端的链接请求。
  • 服务器不是必须调用bind进行绑定,但如果服务器端的代码不调用bind,内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会比较麻烦。
  • 因为此处实现的是单进程操作,所以正常通信的操作是在Server函数中,由本进程运行,如果上述操作都正常且正确,则进入Server函数进行正常通信。

3、运行结果

在这里插入图片描述

  • 127.0.0.1为本地环回地址,因为上方代码是在同一台机器上运行的,IP地址默认为全零。所以,程序所进行的通信操作都是本主机上进行的。

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕

;