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的进程进行通信,结果通信不成功,被防火墙 阻止。

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

相关推荐
xuyanqiangCode7 小时前
Ubuntu二进制安装Apache Doris(2.1版本)
linux·ubuntu·apache
Yue丶越7 小时前
【Python】基础语法入门(四)
linux·开发语言·python
木童6627 小时前
Nginx 深度解析:反向代理与负载均衡、后端Tomcat
linux·运维·nginx
赖small强8 小时前
【Linux 网络基础】网络通信中的组播与广播:基础概念、原理、抓包与应用
linux·网络·broadcast·组播·广播·multicast
陌路208 小时前
Linux是如何收发网络包的?
linux·网络
报错小能手8 小时前
计算机网络自顶向下方法50——链路层 虚拟局域网 链路虚拟化:网络作为链路层(多协议标签交换)
网络·计算机网络·智能路由器
༺ཉི།星陈大海།ཉྀ༻CISSP8 小时前
隐蔽端口穿透攻击的技术分析与防御实践 —基于一次HW行动的实战案例
网络·智能路由器
0wioiw08 小时前
跨网络互联技术(Nginx反向代理)
服务器·网络·nginx
报错小能手9 小时前
计算机网络自顶向下方法55——无线网移动网 移动性管理
网络·计算机网络
带鱼吃猫9 小时前
Linux系统:策略模式实现自定义日志功能
linux·c++