【Linux笔记】Socket编程基础

一、Socket编程基础

1.1 端口号

1.1.1 认识端口号

端口号:是一个 16 位无符号整数,取值范围为 0 ~ 65535。

它并非物理实体,而是操作系统内核维护的一种软件抽象,用于将接收到的数据包正确分发给对应的应用程序(相应的进程)。

核心作用:

1.多路复用与解复用:一台主机可同时运行多个网络应用(Web 服务器、邮件服务、数据库等)。

传输层通过端口号区分这些应用,实现单 IP 支撑多服务。

简而言之:区分同一台主机中的不同进程。

2.服务标识:约定俗成的端口号成为服务的"名字"。

例如:

  • 看到目标端口 80 即 HTTP;

  • 看到目标端口 443 即 HTTPS。

3.连接区分:在 TCP 中,一个四元组(源IP、源端口、目的IP、目的端口)唯一标识一条连接。

同一服务器端口可同时服务成千上万客户端,靠的就是不同的源端口组合。

关键特性:

1.独立于 IP:不同 IP 地址上的相同端口号互不干扰。

如 192.168.1.1:80 与 10.0.0.1:80 是两个独立服务

2.协议隔离:TCP 端口 80 与 UDP 端口 80 是完全独立的命名空间,可同时被不同程序绑定。

3.临时性与持久性并存:服务端端口通常长期固定,客户端端口则动态分配、用完即释放。

1.1.2 端口号的划分

范围 名称 用途 绑定权限 典型示例
0 -- 1023 知名端口 系统级核心服务 需 root/Administrator 22(SSH), 53(DNS), 80(HTTP), 443(HTTPS)
1024 -- 49151 注册端口 用户级应用服务 普通用户即可 3306(MySQL), 5432(PostgreSQL), 8080(HTTP代理), 6379(Redis)
49152 -- 65535 动态/私有端口 客户端临时端口 由内核自动分配 浏览器、curl、API 调用等发起连接时使用

端口 0 :保留不用作目的端口。但在 bind() 时指定端口 0,表示请求内核自动分配一个可用的临时端口。

1.1.3源端口号与目的端口号

源端口号:发送方本次通信使用的本地端口

发送方内核 :在首次发送时自动分配(除非应用显式 bind),分配后仅在本次连接生命周期内有效。

目的端口号:接收方提供服务的监听端口

接收方应用:在服务启动时通过 bind() 显式指定,或由协议标准约定。

属性 源端口号 目的端口号
含义 发送方本次通信使用的本地端口 接收方提供服务的监听端口
分配方式 通常由内核自动分配(临时端口) 由服务端预先绑定(知名/注册端口)
是否固定 每次连接动态变化 服务运行期间保持不变
主要作用 标识发送方,供对方回送响应 标识目标服务,供接收方分用
典型值域 32768--60999(Linux 默认临时范围) 0--49151(知名 + 注册端口)

1.1.4 理解socket

A. 核心概念

Socket(套接字):是操作系统对网络通信端点的抽象,它本质上是一个文件描述符(File Descriptor)。

在 Linux/Unix 哲学中,"一切皆文件",网络通信也被抽象为文件的读写操作。

B. 双重身份

通信端点:代表了网络中的一个通信节点,包含了 IP 地址和端口号的信息。

文件描述符:应用程序通过标准的 I/O 接口(read/write/send/recv)对其进行操作,无需关心底层网络协议的复杂实现。

C. Socket 与协议的关系

Socket 本身不是协议,而是访问传输层服务(TCP/UDP)的统一接口。

创建 Socket 时指定的 domain、type、protocol 决定了该 Socket 遵循哪种网络协议规范。

1.2 传输层的典型代表

1.2.1 认识TCP协议

TCP 的设计目标:是提供可靠、有序、无差错的数据传输服务。

它就像一次确认过的电话通话,双方必须先建立连接,沟通结束后再挂断。

复制代码
TCP的核心特性:
​
1. 传输层协议
​
2. 有连接
​
3. 可靠传输
​
4. 面向字节流

1.2.2 认识UDP协议

UDP 的设计目标:是提供简单、快速、低开销的数据传输服务。

它就像寄明信片,写完地址直接投出去,不确认对方是否收到,也不保证顺序。

复制代码
UDP的核心特性:
​
1. 传输层协议
​
2. 无连接
​
3. 不可靠传输
​
4. 面向数据报

1.3 网络字节序

1.3.3 字节序的本质:大端与小端

字节序指的是:多字节数据(如 16 位、32 位整数)在内存中的存放顺序。

单字节数据不存在字节序问题。

A. 大端字节序

定义:数据的高位字节(Most Significant Byte, MSB)存放在内存的低地址处,低位字节(Least Significant Byte, LSB)存放在内存的高地址处。

示例 :将 32 位整数 0x12345678 存入内存起始地址 0x100:

复制代码
地址:   0x100  0x101  0x102  0x103
内容:   0x12   0x34   0x56   0x78

特点:

  • 与人类书写和阅读数字的顺序一致:从左到右,先看到高位。

  • 网络协议(如 TCP/IP)通常采用大端字节序,因此大端也被称为网络字节序。

B. 小端字节序

定义:数据的低位字节(Least Significant Byte, LSB)存放在内存的低地址处,高位字节(Most Significant Byte, MSB)存放在内存的高地址处。

示例 :同样将 0x12345678 存入内存起始地址 0x100:

复制代码
地址:   0x100  0x101  0x102  0x103
内容:   0x78   0x56   0x34   0x12

特点:

  • 与人类书写顺序相反,但在硬件实现上有优势:低地址直接对应低位,进行加减乘除或数据类型转换时,不需要移动字节位置。

  • 主流处理器(如 x86、大多数 ARM 默认配置)采用小端字节序,因此也叫主机字节序(在 x86 平台上)。

  • 在强制类型转换(如 int 转 char)时,直接取低地址即可得到低位字节,效率较高。

1.3.2 核心原则

网络字节序 = 大端字节序

复制代码
1. 所有在网络上传输的多字节字段(IP 地址、端口号、TCP/UDP 头部字段等)必须以大端序排列。
​
2. 主机内部可使用任意字节序,但在发送前转换为网络字节序,接收后转换回主机字节序。

1.3.3 字节序转换函数

字节序转换函数相关的API接口:

复制代码
#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);
函数 含义 说明
htons() Host to Network Short 16 位主机序 -> 网络序
htonl() Host to Network Long 32 位主机序 -> 网络序
ntohs() Network to Host Short 16 位网络序 -> 主机序
ntohl() Network to Host Long 32 位网络序 -> 主机序

命名记忆法 :h = host, n = network, s = short(16bit), l = long(32bit)

二、 socket编程接口

2.1 socket常见API

复制代码
// 创建 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);

2.2 sockaddr 结构体系列

struct sockaddr:基类的结构体

复制代码
// <sys/socket.h>
struct sockaddr 
{
    sa_family_t sa_family;   // 地址族标识(AF_INET, AF_INET6, AF_UNIX...)
    char        sa_data[14]; // 协议特定地址数据(仅占位,实际不使用)
};

struct sockaddr_in :用于进程间网络通信

复制代码
// <netinet/in.h>

typedef unsigned short int sa_family_t;
typedef uint16_t in_port_t;          // uint16_t 等价于 unsigned short int 
typedef uint32_t in_addr_t s_addr;   // uint32_t 等价于 unsigned int

struct sockaddr_in 
{
    sa_family_t    sin_family;   // ★ sin_family =AF_INET        ---用于网络通信
    in_port_t      sin_port;     // ★ 端口号(网络字节序!)
    struct in_addr sin_addr;     // ★ IPv4 地址(网络字节序!)
    
    unsigned char  sin_zero[8];  // (无需特殊考虑,只用于填充大小)填充至与 sockaddr 等长(16字节)
};

struct in_addr 
{
    in_addr_t s_addr;           // 32位IPv4地址(网络字节序)
};

struct sockaddr_un :用于进程间本地通信

复制代码
// <sys/un.h>

typedef unsigned short int sa_family_t;

struct sockaddr_un 
{
    sa_family_t sun_family;      // ★ sin_family= AF_UNIX / AF_LOCAL  ---用于本地通信
    char        sun_path[108];   // 文件系统路径(以\0结尾)
};

2.3 socket核心接口详解

2.3.1 socket -创建网络通信端点

A. 函数概述
复制代码
1.头文件包含:
    
#include <sys/types.h>          
#include <sys/socket.h>

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

3.函数功能:
    
socket 用于创建一个网络通信端点(endpoint),返回一个文件描述符(file descriptor)
B. 参数详解
复制代码
参数一:int domain(协议族/域) 

核心功能:指定通信所使用的协议族,决定了 socket 的地址格式和底层协议栈。

常见值包括:

- AF_INET:IPv4 ★互联网协议

- AF_INET6:IPv6 互联网协议

- AF_UNIX / AF_LOCAL:★本地进程间通信

- AF_NETLINK:内核与用户空间之间的通信

- AF_PACKET:底层数据包接口(原始套接字)
复制代码
参数二:int type(套接字类型)

核心功能:指定通信的语义和服务质量。

常见值包括:
 
- SOCK_STREAM:面向连接、可靠、有序的字节流(★对应 TCP) 

- SOCK_DGRAM:无连接、不可靠、消息边界保留的数据报(★对应 UDP)
复制代码
参数三:int protocol(具体协议)

核心功能:指定 domain 和 type 下使用的具体协议编号。

常见值:设为0


domain+type 组合下的默认协议:

AF_INET + SOCK_STREAM → 默认为 TCP (IPPROTO_TCP)

AF_INET + SOCK_DGRAM → 默认为 UDP (IPPROTO_UDP)
C. 返回值
复制代码
成功:返回一个非负整数文件描述符

失败:返回 -1,并设置 errno
D. Demo演示
复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>

int main(void)
{
    // 创建 IPv4 TCP socket
    int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    printf("Socket created, fd = %d\n", sockfd);

    // 后续 bind/connect/send/recv ...

    close(sockfd);
    return 0;
}

2.3.2 bind -绑定ip与端口

A.函数原型
复制代码
1.头文件包含:
    
#include <sys/socket.h>

2.函数原型:
    
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

3.函数功能:

将 socket 与本地地址(IP + 端口)绑定。

- 服务器必须先 bind 才能接收连接;

- 客户端通常无需手动 bind(内核会自动分配临时端口)。

注:一个进程可以绑定多个端口号,但一个端口号不能被多个进程所绑定。

B.参数详解
复制代码
参数一: int socket

socket 返回的文件描述符
复制代码
参数二: const struct sockaddr *address
    
指向 sockaddr 结构体的指针,实际传入 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)
复制代码
参数三: socklen_t address_len

地址结构体的大小,通常传 sizeof(struct sockaddr_in)
C.关键结构体
复制代码
struct sockaddr_in 
{
    sa_family_t    sin_family;   ★/* AF_INET */
    in_port_t      sin_port;     ★/* 端口号,网络字节序  */  
    struct in_addr sin_addr;     ★/* IP 地址 -通常设置为INADDR_ANY*/

    char           sin_zero[8];  /* 仅用于填充,无特殊功能,必须置零 */
};
D. 返回值
复制代码
- 成功:返回 0

- 失败:返回 -1,常见 errno:
E. Demo演示
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    /* 设置 SO_REUSEADDR,避免 TIME_WAIT 导致重启时 bind 失败 */
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);              /* 端口 8080,转网络字节序 */
    addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 监听所有网卡 */

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        perror("bind");
        close(sockfd);
        return 1;
    }

    printf("bind 成功: 0.0.0.0:8080, fd=%d\n", sockfd);
    close(sockfd);
    return 0;
}

2.3.3 listen -开启监听

A. 函数原型
复制代码
1.头文件声明: #include <sys/socket.h>

2.函数生声明: int listen(int socket, int backlog);

3.函数功能:
    
- 将 socket 从"主动"状态转为"被动"监听状态,使内核开始接受发往该 socket 的连接请求。
  
  告诉内核:我要开始接收连接了 -> 内核开始处理 TCP 三次握手 -> 维护连接队列
  
  未完成连接队列:收到 SYN,还没完成三次握手。

  已完成连接队列:三次握手已经完成,等待应用程序 accept
    
    
- 仅对 SOCK_STREAM(TCP)有效。
B. 参数详解
复制代码
参数一: socket	

- 已 bind 的 TCP socket 文件描述符


参数二: backlog

- 内核为此 socket 排队的未完成连接队列最大长度。

- Linux 上实际受 /proc/sys/net/core/somaxconn 限制(默认 4096)

补充说明:backlog

  • backlog 过小:高并发时新连接被丢弃,客户端表现为超时或 RST。

  • backlog 过大:占用内核内存,但一般影响不大。

  • 推荐值:根据并发量设定,常见 128、512、1024。

复制代码
客户端 SYN ──→ [半连接队列 (SYN Queue)] ──→ [全连接队列 (Accept Queue)]
                                                  ↑
                                            backlog 限制的是这里
                                                  ↓
                                            accept() 从全连接队列取出
C. 返回值
复制代码
- 成功:返回 0

- 失败:返回 -1

常见 errno:

EOPNOTSUPP:socket 类型不支持 listen(如 UDP)

EADDRNOTAVAIL:socket 未绑定地址
D. Demo演示
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main(void)
{
    int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9090);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        perror("bind");
        close(sockfd);
        return 1;
    }

    if (listen(sockfd, 128) == -1)
    {
        perror("listen");
        close(sockfd);
        return 1;
    }

    printf("listen 成功: 等待连接, backlog=128, fd=%d\n", sockfd);
    printf("按 Ctrl+C 退出...\n");
    pause(); /* 挂起,模拟服务器等待状态 */

    close(sockfd);
    return 0;
}

2.3.4 accept -接受连接 (服务端)

A. 函数原型
复制代码
1. #include <sys/socket.h>

2. 函数声明: int accept(int socket, struct sockaddr *address, socklen_t *address_len);

3. 函数功能:

- 从全连接队列中取出一个已完成的连接,创建一个新的 socket 文件描述符用于与该客户端通信。
    
- 原始监听 socket 继续接受后续连接。
B. 参数详解
复制代码
参数一: socket

- 处于 LISTEN 状态的 socket 文件描述符


参数二: struct sockaddr *address 

- 输出参数,内核填入客户端的地址信息(IP + 端口)。若不需要可传 NULL


参数三: socklen_t *address_len

- 输入/输出参数。调用前填入缓冲区大小,返回时填入实际写入的长度
C. 返回值
复制代码
- 成功:返回新的文件描述符(非负整数),代表与客户端的连接

- 失败:返回 -1

常见 errno:

EAGAIN / EWOULDBLOCK:非阻塞模式下队列为空
ECONNABORTED:连接在队列中已被中止(如客户端发 RST)
EINTR:被信号中断
EMFILE:文件描述符耗尽
D. Demo演示
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (listenfd == -1)
    {
        perror("socket");
        return 1;
    }

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9090);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        perror("bind");
        close(listenfd);
        return 1;
    }
    if (listen(listenfd, 128) == -1)
    {
        perror("listen");
        close(listenfd);
        return 1;
    }

    printf("服务器已就绪,等待客户端连接...\n");

    struct sockaddr_in cliaddr;
    socklen_t cliaddrlen = sizeof(cliaddr);

    /* 阻塞等待客户端连接 */
    int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddrlen);
    if (connfd == -1)
    {
        perror("accept");
        close(listenfd);
        return 1;
    }

    char ip_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cliaddr.sin_addr, ip_str, sizeof(ip_str));
    printf("accept 成功: 客户端 %s:%d, connfd=%d\n",
           ip_str, ntohs(cliaddr.sin_port), connfd);

    close(connfd);
    close(listenfd);
    return 0;
}

2.3.5 connect -发起连接 (客户端)

A. 函数原型
复制代码
1. 头文件包含:#include <sys/socket.h>

2. 函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

3. 函数功能:
    
- 客户端调用此函数向服务器发起 TCP 三次握手。
    
- 成功后 socket 进入 ESTABLISHED 状态,可直接 send()/recv()。
B. 参数详解
复制代码
参数一: sockfd

- socket() 返回的文件描述符。

参数二: addr

- 指向目标服务器地址结构体的指针(通常为 struct sockaddr_in)。

参数三: addrlen

- 地址结构体的大小,通常为 sizeof(struct sockaddr_in)。
C. 返回值
复制代码
成功:返回 0

失败:返回 -1

常见 errno:

ECONNREFUSED:目标端口未监听或服务器拒绝连接
ETIMEDOUT:连接超时(网络不可达或防火墙丢包)
EINPROGRESS:非阻塞模式下连接正在进行中
ENETUNREACH:网络不可达
D. Demo演示
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(9090);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        perror("connect");
        close(sockfd);
        return 1;
    }

    printf("connect 成功: 已连接到 127.0.0.1:9090\n");
    
    // 后续可进行 send/recv 操作...
    
    close(sockfd);
    return 0;
}

2.4 socket其他函数接口

2.4.1 inet_pton -ip转换字符串

"点分十进制的字符串" 转换为 "4字节网络风格的IP"

复制代码
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);

参数一 af:  AF_INET / AF_INET6
    
参数二 src: 源字符串
    
参数三 dst: 输出x,一般传入 struct in_addr 类型

2.4.2 inet_ntop -字符串转换ip

"4字节网络风格的IP" 转换为 "点分十进制的字符串"

复制代码
#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

2.4.3 recvfrom -接受消息

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

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

2.4.4 sendto -发送消息

复制代码
#include <sys/types.h>
#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);

2.4.5 recv -接受消息

复制代码
1.头文件包含:#include <sys/socket.h>

2.函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);

3.函数功能:

- 从已连接的 socket 中接收数据。

- 仅适用于面向连接的 socket(TCP)

2.4.6 send -发送消息

复制代码
1.头文件包含:#include <sys/socket.h>

2.函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);

3.函数功能:

- 向已连接的 socket 发送数据。

- 仅适用于面向连接的 socket(TCP)
相关推荐
高山有多高1 小时前
【Linux笔记】UpdSocket
linux·运维·笔记
皓月盈江1 小时前
Linux系统grep、sed 、awk介绍下,使用方法及区别?
linux·运维·服务器·sed·grep·awk
云栖梦泽2 小时前
Linux内存管理
linux·运维·服务器
Lucis__2 小时前
编译链接全链路解析:ELF形成到可执行加载的发展历程
linux·程序·elf文件
ken22322 小时前
ubuntu 24.04, audacity 4 AppImage 加载 libavdevice.so.62 /.61 /.63 失败:依赖缺失?
linux·运维·ubuntu
NightReader2 小时前
vCenter8部署
linux
高山有多高3 小时前
【Linux笔记】线程概念与控制
linux
阳光九叶草LXGZXJ3 小时前
达梦数据库-学习-69-DM9主备集群部署
linux·运维·服务器·数据库·sql·学习
Sun 32853 小时前
Linux 常用命令梳理
linux·运维·服务器·开发·常用命令