cen's blog cen's blog
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • CLion
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

cen

十年饮冰,难凉热血
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • CLion
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • 网络基础
  • 套接字和UDP
    • 前置知识
      • 理解地址
      • 认识端口号
      • TCP 协议和 UDP 协议
      • 网络字节序
    • socket 编程接口
      • 常用 API
      • sockaddr 结构体
    • 简单的 UDP 网络程序
      • 实现
      • sockaddr_in 结构体
      • IP 地址的表示
      • 服务端
      • 客户端
      • 数据的发送和读取
  • 套接字和TCP
  • 网络协议
  • 网络
cen
2025-08-16
目录

套接字和UDP

# 前置知识

# 理解地址

数据在传输的过程中是有两套地址:

  1. 源 IP 地址和目的 IP 地址,在网络层封装的 IP 报头当中就涵盖了源 IP 地址和目的 IP 地址,这两个地址在数据传输过程中基本是不会发生变化的
  2. 源 MAC 地址和目的 MAC 地址,这两个地址是一直在发生变化的,因为在数据传输的过程中路由器不断在进行解包和重新封装。

# 认识端口号

端口号(port)是传输层协议的内容。端口号是一个 2 字节 16 位的整数,用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。

通常而言:IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程,而且一个端口号只能被一个进程占用。传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁"。

端口号(port)的作用唯一标识一台主机上的某个进程,进行网络通信时为什么不用 pid 来代替 port 呢?

  • 系统和网络需要单独设置,系统与网络解耦
  • 所有的进程都需要 pid,但不是所有的进程需要提供网络服务或请求
  • 需要客户端每次能找到服务器进程,所以服务器要确保唯一性

# TCP 协议和 UDP 协议

TCP(传输控制协议)协议和 UDP(用户数据协议)协议是两个传输层协议,TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议;UDP 是一种无连接的、不可靠的、面向报文的传输层通信协议。

TCP 的可靠性是通过一系列复杂的机制(三次握手、确认、重传、排序、流量控制、拥塞控制)来实现的,这些机制每一步都会引入延迟和开销,但速度和成本都上去了;UDP 则直接把数据包扔向网络,用可靠性换来了速度、效率和低延迟。所以编写网络通信代码时具体采用 TCP 协议还是 UDP 协议,完全取决于上层的应用场景。

# 网络字节序

内存中数据存储有大小端之分:

例如存储整形 1:

  • 大端:数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。00 00 00 01
  • 小端:数据的低位字节内容保存在内存的低地址处,⽽数据的高位字节内容,保存在内存的高地址处。01 00 00 00

网络中,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

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);
1
2
3
4
5
6

解释:

  • 函数名当中的 h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
  • 如果主机是小端字节序,则这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,则这些函数不做任何转换,将参数原封不动地返回。

# socket 编程接口

# 常用 API

包含头文件#include <sys/socket.h>

  1. socket
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
1
2

参数:

  • domain:创建套接字的域或者协议家族
  • type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于 UDP 的网络通信,我们采用的就是 SOCK_DGRAM,叫做用户数据报服务,如果是基于 TCP 的网络通信,我们采用的就是 SOCK_STREAM,叫做流式套接字,提供的是流式服务
  • protocol:设置为 0 默认即可

返回值:创建成功返回文件描述符(一切皆文件),失败则返回-1

  1. bind
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
1
2

参数:

  • sockfd:绑定的文件的文件描述符
  • address:网络相关的属性信息,包括协议家族、IP 地址、端口号等
  • address_len:传入的 address 结构体的长度

返回值说明:绑定成功返回 0,绑定失败返回-1

  1. listen
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
1
2

参数:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符。
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为 5 或 10 即可。
  1. accept

TCP 协议中,服务器需要先获取到客户端的连接请求,才能进行网络通信

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
1
2

参数:

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等。
  • addrlen:调用时传入期望读取的 addr 结构体的长度,返回时代表实际读取到的 addr 结构体的长度,这是一个输入输出型参数。

返回值:获取连接成功返回接收到的套接字的文件描述符,否则返回 -1

调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
  1. connect
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
1
2

参数:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等。
  • addrlen:传入的 addr 结构体的长度。

返回值:连接或绑定成功返回 0,连接失败返回-1

# sockaddr 结构体

socket 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及本地的进程间通信(域间套接字)。然而,各种网络协议的地址格式并不相同,因此套接字提供了sockaddr_in(跨网络通信)结构体和sockaddr_un(本地通信)结构体。

struct sockaddr {
    sa_family_t sa_family;    // 地址家族(Address Family)
    char        sa_data[14];  // 协议地址
};
1
2
3
4

在传递在传参时,统一传入 sockeaddr 这样的结构体,在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信。(IPv4、IPv6 地址类型分别定义为常数 AF_INET、AF_INET6)

netstat

# 简单的 UDP 网络程序

# 实现

udpService.hpp


#include <arpa/inet.h>
#include <strings.h>
#include <sys/socket.h>

#include <errno.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <iostream>

namespace Server {

using namespace std;

enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR };

const string DEFAULT_IP = "0.0.0.0";

#define gnum 1024

class udpServer {
public:
    udpServer(const uint16_t& _port, const string& _ip = DEFAULT_IP)
        : port(_port), ip(_ip), fd(-1) {}

    void initServer() {
        // 1. 创建套接字
        fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd == -1) {
            cerr << "socket error:" << errno << ":" << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }

        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        // 任意地址绑定
        // local.sin_addr.s_addr = INETADD_ANY;

        // 2. 绑定 port、ip
        int ret = bind(fd, (struct sockaddr*)&local, sizeof(local));
        if (ret == -1) {
            cerr << "bind error:" << errno << ":" << strerror(errno) << endl;
            exit(BIND_ERR);
        }
    }

    void start() {
        char buffer[gnum];
        while (true) {
            // 服务器读取数据:
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n =
                recvfrom(fd, buffer, gnum - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0) {
                // success:
                string clientIp = inet_ntoa(peer.sin_addr);
                uint16_t clientPort = ntohs(peer.sin_port);
                buffer[n] = 0;
                string message = buffer;
                cout << "[" << clientIp + "-" << clientPort << "]# " << message
                     << endl;
            }
        }
    }

    ~udpServer() {
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
    }

private:
    // uint16_t : unsigned int 16 bit
    uint16_t port;
    string ip;
    int fd;
};
}  // namespace Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

udpClient.hpp

#include <arpa/inet.h>
#include <strings.h>
#include <sys/socket.h>

#include <errno.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <iostream>

namespace Client {

using namespace std;

enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR };

class udpClient {
public:
    udpClient(const string& _serverIp, const uint16_t& _serverPort)
        : serverIp(_serverIp), serverPort(_serverPort), fd(-1) {}

    void initClient() {
        fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd == -1) {
            cerr << "socket error:" << errno << ":" << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
    }

    void run() {
        string message;
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(serverIp.c_str());
        server.sin_port = htons(serverPort);
        while (true) {
            cout << "[client-Input message]#";
            cin >> message;
            sendto(fd, message.c_str(), sizeof(message), 0, (sockaddr*)&server,
                   sizeof(server));
        }
    }

    ~udpClient() {
        if (fd != -1) {
            close(fd);
            fd = -1;
        }
    }

private:
    string serverIp;
    uint16_t serverPort;
    int fd;
};
}  // namespace Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

udpService.cpp

#include "udpServer.hpp"

#include <memory>

using namespace Server;

void Usage() {
    std::cout << "Usage form: ./udpServer port" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage();
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<udpServer> server(new udpServer(port));
    server->initServer();
    server->start();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

udpClient.cpp

#include "udpClient.hpp"

#include <memory>

using namespace Client;

void Usage() {
    std::cout << "Usage form: ./udpClient server_ip server_port" << std::endl;
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage();
        exit(USAGE_ERR);
    }
    string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    std::unique_ptr<udpClient> cilent(new udpClient(server_ip, server_port));

    cilent->initClient();
    cilent->run();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 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)];
  };
1
2
3
4
5
6
7
8
9
10
11
12

使用:

struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
1
2
3
4
5

# IP 地址的表示

  • 字符串表示:可读性高,叫做点分十进制 IP 地址,如:192.168.1.1
  • 整数表示:用于程序内部处理、数据库存储、网络计算

Linux 系统中提供了二者转换的函数:

  • 字符串 IP 转换成整数 IP
in_addr_t inet_addr(const char *cp);
1
  • 整数 IP 转换成字符串 IP
char *inet_ntoa(struct in_addr in);
1

# 服务端

    void initServer() {
        // 1. 创建套接字
        fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd == -1) {
            cerr << "socket error:" << errno << ":" << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }

        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        // 任意地址绑定
        // local.sin_addr.s_addr = INETADD_ANY;

        // 2. 绑定 port、ip
        int ret = bind(fd, (struct sockaddr*)&local, sizeof(local));
        if (ret == -1) {
            cerr << "bind error:" << errno << ":" << strerror(errno) << endl;
            exit(BIND_ERR);
        }
    }

    void start() {
        char buffer[gnum];
        while (true) {
            // 3. 服务器读取数据:
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(fd, buffer, gnum - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0) {
                // success:
                string clientIp = inet_ntoa(peer.sin_addr);
                uint16_t clientPort = ntohs(peer.sin_port);
                buffer[n] = 0;
                string message = buffer;
                cout << "[" << clientIp + "-" << clientPort << "]# " << message << endl;
                callback(fd, clientIp, clientPort, message);
            }
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 客户端

    static void* readerMessage(void* args) {
        pthread_detach(pthread_self());
        int socketfd = *(static_cast<int*>(args));

        while (true) {
            char buffer[1024];
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t n = recvfrom(socketfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&temp, &len);

            if (n > 0) {
                buffer[n] = 0;
                cout << buffer << endl;
            }
        }

        return nullptr;
    }
    void initClient() {
        fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd == -1) {
            cerr << "socket error:" << errno << ":" << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
    }

    void run() {
        // 主线程写、读进程读

        pthread_create(&reader, nullptr, readerMessage, (void*)&fd);

        string message;
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(serverIp.c_str());
        server.sin_port = htons(serverPort);
        while (true) {
            fprintf(stderr, "Please Input# ");
            fflush(stderr);
            getline(cin, message);
            sendto(fd, message.c_str(), sizeof(message), 0, (sockaddr*)&server, sizeof(server));
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

解释: 网络通信就像开店和顾客的关系:

  • 服务器:像一家店铺。必须有固定的地址(IP)和门牌号(端口),这样顾客才能找到它。
  • 客户端:像顾客。每次来购物时,从哪里来(使用哪个端口)并不重要,只要他能找到店铺并完成交易就行。

所以:服务器通过绑定来固定端口,提供稳定的服务;客户端通过系统分配使用临时端口(不需要显示地绑定端口),保证灵活的访问。

# 数据的发送和读取

  • 读取数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
1

参数:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。

  • buf:读取数据的存放位置。

  • len:期望读取数据的字节数。

  • flags:读取的方式。一般设置为 0,表示阻塞读取。

  • src_addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等。

  • addrlen:调用时传入期望读取的 src_addr 结构体的长度,返回时代表实际读取到的 src_addr 结构体的长度,这是一个输入输出型参数。

  • 发送数据

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

参数:

  • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为 0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等。
  • addrlen:传入 dest_addr 结构体的长度。
上次更新: 2025/09/03, 18:26:17
网络基础
套接字和TCP

← 网络基础 套接字和TCP→

最近更新
01
网络协议
09-03
02
套接字和TCP
08-26
03
常用数据结构
08-23
更多文章>
Theme by Vdoing | Copyright © 2024-2025 京ICP备2020044002号-3 京公网安备11010502056119号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式