Linux网络(二)——socket编程

端口号

概念

端口号是一个16位的无符号整数 ,取值范围为 0~65535(共 2^16 = 65536 个)。用来标识一台设备上特定网络进程 的数字标识。总结 :IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程。

  1. 我们上网无非就两种动作:a.把远处的数据拉取到本地 b.把我们数据发送到远端
  2. 大部分的网络通信行为,都是用户触发的。计算机中谁表示用户? 进程
  3. 把数据发送到目标主机不是目的是手段,真正的目的是把数据交给主机上的某个服务(进程)
  4. 网络通信的本质,其实是进程在帮我们进行网络通信
  5. IP(唯一的一台主机)+port(该主机上的唯一的一个进程)=> 互联网中唯一的一个进程
  6. 客户端与服务端进行通信→客户端进程 与服务端进程 进行通信
    client进程 = client ip + client port => client是互联网中唯一的一个进程
    server进程 = server ip + server port => server是互联网中唯一的一个进程
    所以可以唯一地找到彼此。

总结一下
网络通信的本质 :其实就是进程间通信
IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程。

只要有源IP地址+源端口号port ,目标IP地址+目标端口号port,就可以唯一确定两个进程的位置,从而进行通信。

这种基于IP地址 +端口号port 的通信称之为套接字通信(socket通信)。

端口号的范围

0-1023 :知名端口号,Http,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。
1024-65535:操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的。

进程标识符pid vs 端口号port

PID(Process ID,进程标识符) 是系统为每个正在运行的进程分配的唯一整数编号,用于标识和管理进程。

都可以唯一的标识一个进程,为什么还要设计出这两种呢?统一用一个行不行?

  1. OS中每一个进程都要有pid,但是不是每一个进程都要有port(需要网络服务的进程才有)。
  2. 不要让pid与网络服务强耦合在一起,专事专办,让端口号port专门负责网络相关的事物。

初识TCP协议与UDP协议

TCP协议 :传输层协议,有连接,可靠传输,面向字节流
UDP协议:传输层协议,无连接,不可靠传输,面向数据报

TCP vs UDP 可靠性

TCP要保证可靠性,就要做更多工作------tcp协议更复杂,接口会多一点。

UDP协议不可靠,但更简单。

网络字节序

字节序 特性
大端字节序 多字节数据的 "高位字节 "存放在内存的 "低地址"
小端字节序 多字节数据的 "高位字节 "存放在内存的 "高地址"

内存中的多字节数据相对于内存地址有大端和小端之分,网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢

  1. 发送主机 通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  2. 接受主机 把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存

因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
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位短整数。

htonl → h to n l → host to network long,表示将32位长整数 (long)从主机字节序 (host)转换(to)到网络字节序(network)

注:端口用 s(16 位),IP 用 l(32 位) 。

16 位数据(端口号)→ htons/ntohs

32 位数据(IPv4 地址)→ htonl/ntohl

socket编程接口

socket常见API

c 复制代码
//创建socket文件描述符(TCP/UDP,客户端 + 服务器)
int socket(int domain, int type, int protocol);
//绑定端口号(TCP/UDP, 服务器)
int bind(int socket,const struct sockaddr *address, socklen_t* address_len);
//开始监听socket(TCP,服务器)
int listen(int socket, int backlog);
//接收请求(TCP,服务器)
int accept(int socket, struct sockaddr *address, socklen_t* address_len);
//建立连接(TCP,服务器)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

总结:socket相关头文件(socket编程必备)编程时都加上

c 复制代码
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

struct sockaddr结构体

上述常见API接口中几乎都有一个这样的结构体struct sockaddr *address,我们详细介绍一下。

struct sockaddr

它是通用套接字地址结构体 ,是其他具体地址结构体的 "父类 "(用于类型兼容)。
注意:实际上C语言中没有多态继承这样的概念,而这个结构体中模拟实现这个方法。

包含 16 位地址类型 (用于标识地址的协议族,如AF_INET表示 IPv4、AF_UNIX表示本地套接字等)。
包含 14 字节地址数据(用于存储具体的地址信息,不同协议族的地址会在此处有不同的结构,因此通常需要结合地址类型做类型转换后使用)。

socket编程,是有不同种类的 :

有的是专门用来进行本地通信 的------unix socket

有的是用来专门进行跨网络通信 的------inet socket

有的是用来进行网络管理的------raw socket

那么如何区分是哪种socket编程?通过struct sockaddr结构体来确定!通过struct sockaddr结构体中这个16 位地址类型的数据来判别。

struct sockaddr_in------用于跨网络通信

它是IPv4 协议的套接字地址结构体,专门用于描述 IPv4 网络中的地址和端口。(简单理解为跨网络通信)
16 位地址类型 :固定为AF_INET (标识这是 IPv4 地址)。
16 位端口号 :用于标识网络应用的端口(如 HTTP 的 80 端口、HTTPS 的 443 端口)。
32 位 IP 地址 :存储 IPv4 地址(如192.168.1.1这类地址的二进制形式)。
8 字节填充:用于补齐结构体长度,保证内存对齐(不承载实际数据,仅为兼容内存布局)。

struct sockaddr_un------用于本地通信

它是本地 套接字的地址结构体,用于同一台主机内的进程间通信 。
16 位地址类型 :固定为AF_UNIX (标识这是本地套接字地址)。
108 字节路径名:存储本地文件系统的路径(进程通过这个路径来标识和连接通信端点)。

socket常见的API中 struct sockaddr *address参数是如何识别本地通信还是跨网络通信?

所有的结构体(无论是struct sockaddr_in还是struct sockaddr_un)统一被强转成 struct sockaddr结构体传入,如何被识别?

通过判断16 位地址类型 是AF_INET还是AF_UNIX,来判别是struct sockaddr_in还是struct sockaddr_un,从而使用对应的数据结构类型。

关于struct sockaddr类型结构体的填充

难点 是第二个参数struct sockaddr类型结构体的填充。

双方通信的各自的IP地址和port数据是要发送给对方的,换句话说,是要经过网络传输的。

c 复制代码
struct sockaddr_in local;//#include <netinet/in.h>系统中的数据结构
bzero(&local, sizeof(local));//初始化结构体//memset(&local,0,sizeof(local));
//1.确认网络协议族(跨网络,还是本地通信)
local.sin_family = AF_INET;//跨网络通信
//2.填充端口号
local.sin_port = htons(_port);//端口号要经过网络传送的,小细节:需要主机序列to网络序列
//3.填充IP地址(分两步)
//a.字符串风格的点十进制的IP地址->4字节IP地址
//b.主机序列,转成网络序列
//in_addr_t inet_addr(const char *cp);同时完成a和b
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //"192.168.3.1"字符串风格的点十进制的IP地址->4字节IP地址 

如何理解"192.168.1.2"<==>4字节IP地址之间相互转化?(大致原理)

c 复制代码
string ip_str = "192.168.1.2";//ip字符串
c 复制代码
uint32_t ipaddr = xxxxx;//四字节IP地址

需要一个结构体进行转换

c 复制代码
struct IP
{
	uint8_t p1;
	uint8_t p2;
	uint8_t p3;
	uint8_t p4;	
}

4字节IP地址→字符串ip地址

c 复制代码
struct IP* temp = (struct IP*)&ipaddr ;
string ip_str = to_string(temp->p1)+"."+to_string(temp->p2)+"."+to_string(temp->p3)+"."+to_string(temp->p4);//将4字节IP地址转换成字符串ip地址

字符串ip地址→4字节IP地址

c 复制代码
struct IP temp;


size_t pos1 = ip_str.find('.');
temp.p1 = stoi(ip_str.substr(0, pos1));

size_t pos2 = ip_str.find('.', pos1 + 1);
temp.p2 = stoi(ip_str.substr(pos1 + 1, pos2 - pos1 - 1)));

size_t pos3 = ip_str.find('.', pos2 + 1);
temp.p3 = stoi(ip_str.substr(pos2 + 1, pos3 - pos2 - 1));

temp.p4 = stoi(ip_str.substr(pos3 + 1));

uint32_t ipint = (int)temp;

用inet_addr函数来实现上述过程:点分十进制格式的 IPv4 地址→4字节IP地址(进一步转化成网络字节序)

inet_addr函数

c 复制代码
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);

用于将点分十进制格式的 IPv4 地址(例如 "192.168.1.1")→32 位无符号整数(网络字节序)

具体来说,分成两步:

  1. 字符串 → 4 字节二进制(主机字节序)
  2. 主机字节序 → 网络字节序(大端)

inet_ntoa函数

用于将 IPv4 地址的 32 位网络字节序 二进制值→人类可读的点分十进制字符串(与 inet_addr 功能相反)

c 复制代码
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

in :struct in_addr 类型的结构体,其成员 s_addr 存储着 32 位网络字节序的 IPv4 地址(通常来自 struct sockaddr_in 的 sin_addr 字段 )。

struct in_addr 类型的结构体定义如下:

c 复制代码
#include <netinet/in.h>

struct in_addr {
    in_addr_t s_addr;  // 32位无符号整数,存储网络字节序的IPv4地址
};

socket函数

用于初始化一个网络通信端点。

c 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明

1.domain(协议域 / 地址族)

指定套接字使用的网络协议族,决定了通信的地址类型(如 IPv4、IPv6、本地进程间通信等)。常见取值:

  • AF_INET:IPv4 协议(最常用)。
  • AF_INET6:IPv6 协议。
  • AF_UNIX:本地套接字(同一主机内进程间通信,通过文件系统路径标识)。

2.type(套接字类型)

指定套接字的通信方式(面向连接或无连接),常见取值:

  • SOCK_STREAM :流式套接字(面向连接,可靠传输)。
    基于 TCP 协议,保证数据有序、无丢失、无重复,适用于 HTTP、FTP 等需要可靠传输的场景。
  • SOCK_DGRAM :数据报套接字(无连接,不可靠传输)。
    基于 UDP 协议,不保证数据到达顺序和完整性,但传输效率高,适用于 DNS、视频流等对实时性要求高的场景。
  • SOCK_RAW:原始套接字(直接操作底层协议,如 ICMP)。
    可绕过 TCP/UDP 协议栈,用于自定义协议或网络工具(如 ping),需要 root 权限。

3.protocol(协议)

指定具体使用的协议 ,通常设为 0 (由系统根据 domain 和 type 自动选择默认协议 ):

若 domain=AF_INET 且 type=SOCK_STREAM,默认协议为 IPPROTO_TCP(TCP)。

若 domain=AF_INET 且 type=SOCK_DGRAM,默认协议为 IPPROTO_UDP(UDP)。

返回值
成功 :返回一个非负整数(套接字描述符,类似文件描述符 ,用于后续操作)。
失败:返回 -1,并设置 errno 表示错误原因。

bind函数

将套接字 sockfd 与一个 ** 本地地址(IP + 端口或本地路径)** 绑定,让操作系统明确该套接字 "属于哪个通信端点"。

c 复制代码
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明
sockfd :socket() 函数返回的套接字描述符(标识要绑定的套接字)。
addr :指向 struct sockaddr 类型的指针(需根据协议族转换为具体结构体 ,如 sockaddr_in 用于 IPv4、sockaddr_un 用于本地套接字)。
addrlen:addr 结构体的长度(用于告知系统地址结构的大小)。

返回值
成功 :返回 0。
失败:返回 -1,并设置 errno。

netstat 命令 :查看 UDP 网络连接状态,通常跟选项-anup

-a(all):显示所有网络连接

-u(udp):仅显示UDP 协议的连接

-n(num):以数字形式显示 IP 地址和端口号(而非域名或服务名,如直接显示 192.168.1.1:8080 而非 example.com:http)

-p(pid):显示每个连接对应的进程 ID(PID)和进程名


IP地址127.0.0.1本地环回

可以实现本地通信,常用于进行代码测试。

recvfrom函数

网络编程中用于接收数据报(UDP) 的系统调用,它不仅能接收数据,还能获取发送方的地址信息(IP 和端口)

c 复制代码
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明
sockfd :套接字描述符(通过 socket() 创建的 UDP 套接字)。
buf :输出型参数 ,指向接收缓冲区的指针(用于存储接收到的数据)。
len :缓冲区的大小(字节数),期望接受长度 。
flags :接收方式标志,通常设为 0(默认阻塞接收)。
src_addr :输出型参数 ,指向 struct sockaddr 类型的指针(用于存储发送方的地址信息 ,如 IPv4 地址和端口)。若不需要获取发送方地址,可设为 NULL。
addrlen :输入输出参数:
输入 时:src_addr 缓冲区的大小(如 sizeof(struct sockaddr_in))。
输出 时:实际 存储的地址信息长度(由系统填充)。

若 src_addr 为 NULL,此参数也需设为 NULL。

返回值
成功 :返回实际 接收到的字节数(可能小于 len,因为 UDP 数据报有最大长度限制)。
失败:返回 -1,并设置 errno。

sendto函数

网络编程中用于发送数据报(UDP) 的系统调用,专门用于无连接协议(如 UDP),可以指定数据的接收方地址(IP 和端口)。

c 复制代码
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明
sockfd :套接字描述符(通过 socket() 创建的 UDP 套接字,类型为 SOCK_DGRAM)。
buf :指向待发送数据的缓冲区指针(存储要发送的内容 )。
len :期待 发送数据的长度(字节数)。注意:UDP 数据报有最大长度限制(通常约 65507 字节,超过会被截断或失败)。
flags :发送方式标志,通常设为 0(默认行为)。
dest_addr :指向 struct sockaddr 类型的指针(存储接收方的地址信息,如 IPv4 地址和端口)。需根据协议族转换为具体结构体(如 sockaddr_in 用于 IPv4)。
addrlen:dest_addr 结构体的长度(如 sizeof(struct sockaddr_in))。

返回值
成功 :返回实际 发送的字节数(通常等于 len,但特殊情况下可能小于)。
失败:返回 -1,并设置 errno。

socket编程实例

编写一个客户端与服务端来回通信的例子。

流程:客户端输入信息→服务端收到信息,并打印信息来源的主机IP和发送消息进程的端口号→服务端回信(回复相同内容)→客户端收到来信

udpserver服务端

服务端核心代码

c 复制代码
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include "InetAddr.hpp"
//socket相关头文件
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR
};

const static int defaultfd = -1;
class UdpServer
{
public:
    UdpServer(const std::string &ip, uint32_t port):_sockfd(defaultfd), _port(port), _ip(ip)
    {}
    ~UdpServer()
    {

    }

    void Initserver()
    {
        //1.创建udp socket套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            std::cerr << "socket create error!" << std::endl;
            exit(SOCKET_ERROR);
        }
        std::cout << "socket create success!" << std::endl;

        //2.0 填充sockaddr_in结构体变量
        struct sockaddr_in local;//#include <arpa/inet.h> #include <netinet/in.h>系统中的数据结构
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//端口号要经过网络传送的,小细节:需要主机序列to网络序列
        //a.字符串风格的点十进制的IP地址->4字节IP地址
        //b.主机序列,转成网络序列
        //in_addr_t inet_addr(const char *cp);同时完成a和b
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); //"192.168.3.1"字符串风格的点十进制的IP地址->4字节IP地址 
        //local.sin_addr.s_addr = INADDR_ANY;//htonl(INADDR_ANY);//绑定本机所有网卡地址

        //2.1 绑定端口号和ip地址
        int n = bind(_sockfd,(struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            std::cerr << "bind error!" << std::endl;
            exit(BIND_ERROR);
        }
        std::cout << "bind success!" << std::endl;
    }
    bool start()
    {
        //一直运行,直到管理者不想运行了(服务器是死循环)
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer);
            //1.我们要让server先收数据
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &peerlen);
            if(n > 0)
            {
                buffer[n] = 0;
                InetAddr addr(peer);
                std::cout << "get message:" << buffer << " from " << addr.IP() << ":" << addr.Port() << std::endl;
            }
            //2.将server收到的数据发送给对方
            sendto(_sockfd, buffer, n, 0, (struct sockaddr*)&peer, peerlen);
        }
        _isrunning = false;
        return _isrunning;
    }
    
private:
    int _sockfd;
    uint16_t _port;//服务器所用端口号
    std::string _ip;//服务器所用IP地址,暂时
    bool _isrunning;
};

主程序

c 复制代码
#include<iostream>
#include<memory>
#include"UdpServer.hpp"

//./udpserver ip port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: ./udpserver ip port" << std::endl;
        exit(USAGE_ERROR);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(ip, port);
    user->Initserver();
    user->start();
    return 0;
}

用于获取发送方IP及port

c 复制代码
class InetAddr
{
private:
    void GetAddress(std::string *ip, uint16_t *port)
    {
        *port = ntohs(_addr.sin_port);
        *ip = inet_ntoa(_addr.sin_addr);
    }
public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        GetAddress(&_ip, &_port);
    }
    std::string IP()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {

    }
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

我们服务器无法直接绑定(bind)公网IP(云服务器不允许)。即使能绑定,我们也严重不推荐(不推荐bind公网IP,或者任何一个确定的IP )。
原因:

  1. 若后续服务器增加网卡、切换 IP 或迁移到新节点,绑定固定 IP 的程序会失效,必须修改代码重新部署。
  2. 现代服务器常配置多个 IP,绑定单个 IP 会导致其他 IP 无法提供服务。

服务器通常绑定 0.0.0.0 或 INADDR_ANY :表示任意地址绑定,INADDR_ANY(宏定义,值为 0)
0.0.0.0(INADDR_ANY)的本质 :"监听所有可用地址"

0.0.0.0 不是一个可直接通信的 IP 地址,而是一个通配符 ,表示 "绑定到本机所有已配置的 IP 地址"。例如:若服务器有内网 IP(如 172.16.0.10)、虚拟 IP(如 10.0.0.5),绑定 0.0.0.0 后,客户端通过任何一个 IP 都能访问服务器。

拓展 :127.0.0.1 是 IPv4 中的本地环回地址

发送到 127.0.0.1 的数据不会通过物理网卡发送到外部网络,而是直接在本机的 TCP/IP 协议栈内部 "环回",被本机的网络程序接收 。

整个 127.0.0.0/8 网段(从 127.0.0.1 到 127.255.255.254)都属于环回地址,其中 127.0.0.1 是最常用的默认地址,通常与主机名 localhost 绑定。

开发网络程序时,无需外部网络,直接用 127.0.0.1 即可测试通信逻辑。例如:服务器绑定 127.0.0.1:8080,客户端向 127.0.0.1:8080 发送数据,数据在本机内部传输,快速验证代码正确性。

127.0.0.1与 0.0.0.0地址辨析
127.0.0.1 :仅接收来自本机的连接 ,外部设备无法通过此地址访问本机服务。
0.0.0.0 :监听本机所有网络接口(包括内网 IP、公网 IP),允许外部设备访问。

udpclient客户端

c 复制代码
#include <iostream>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>

//./udpclient ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " <ip> <port>" << std::endl;
        return 1;
    }

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

    //1.创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket create error!" << std::endl;
    }
    //2.client要不要bind?一定要,因为client也有自己的ip和port。
    //要不要显示bind[和server一样用bind函数]?不能,系统会自动分配一个未被使用的port号

    //a.如何bind呢? udp client首次发送数据的时候,系统会自动随机给client进行bind端口号 --- 为什么?要防止client port冲突,一个进程只能和一个端口号绑定
    //b.什么时候bind呢? client首次发送数据的时候。

    //构建目标主机的socket信息
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(serverport);
    serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str());

    std::string message;
    while(true)
    {
        std::cout<< "Please Enter# ";
        std::getline(std::cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

        struct sockaddr_in peer;
        socklen_t peerlen = sizeof(peer);
        char buffer[1024];
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &peerlen);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    return 0;
}

客户端的细节------关于创建socket后是否bind?

直接给出答案:需要bind,但是系统帮我们隐式bind(自动bind),不用我们手动bind。

客户端的核心需求 是 "能与服务器通信 ",而不是 "绑定固定端口"。

  1. 服务器需要绑定固定端口,因为客户端需要知道 "连接哪个端口" 才能找到服务器。
  2. 客户端则相反:只要端口唯一(不与其他进程冲突),临时分配一个即可。操作系统会在客户端首次调用 sendto(或 connect)时,自动从 "临时端口范围"(通常是 1024~65535)中分配一个未被使用的端口,并隐式完成 bind。

如果客户端显式调用 bind 绑定固定端口,会带来两个问题:

  1. 端口冲突风险:如果绑定的端口已被其他进程占用 ,bind 会失败,导致客户端无法启动。
  2. 灵活性差:客户端通常不需要固定端口,强制绑定会限制程序的通用性(例如同一台机器启动多个客户端实例时,固定端口会导致冲突)。

总结:
客户端首次调用 sendto 发送数据时,操作系统会自动执行以下操作(隐式 bind):

  1. 从临时端口池选择一个未被占用的端口。
  2. 将客户端套接字与 "本机 IP + 选中的临时端口" 绑定。
  3. 发送数据时,使用这个绑定的地址 作为源地址。

对例子进行测试,以下是运行的大致流程。

服务端输入0.0.0.0与要绑定的端口号,客户端输入公网IP与服务端绑定的端口号,准备进行通信

客户端输入信息→服务端收到信息,并打印信息来源的主机IP和发送消息进程的端口号→服务端回信(回复相同内容)→客户端收到来信

使用本地环回IP

注:云服务器的port默认是禁止访问的,需要自己去手动开放。

我在云服务器的安全组中仅开放了端口号8888

在本地环回IP中,服务端的端口号设置为8887,由于是本地通信,本次通信成功。(本地通信只是自己走一遍协议栈,不需要经过网络,不会被防火墙阻止 )

我要进行网络通信,服务端的端口号设置为8887,绑定IP地址0.0.0.0与端口号8887,这时通过网络 向公网IP+端口号8887的进程进行通信,结果通信不成功,被防火墙 阻止。

把安全组配置放开,才能在相应端口进行网络通信。

相关推荐
律宏阔9 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔9 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴9 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千9 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei379 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei379 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei379 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW9 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW9 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei3710 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化