目录
[3.1 概念](#3.1 概念)
[3.2 常用API](#3.2 常用API)
[4.1 概念](#4.1 概念)
[4.2 常用API](#4.2 常用API)
[4.3 地址转换函数](#4.3 地址转换函数)
一、端口号
网络协议栈中的下三层,主要解决的是如何将数据安全可靠的送到远端机器上的问题。
在上层,用户使用特定的应用层软件完成数据的发送和接收。而软件在启动后变为了进程,因此我们日常网络通信的本质,就是进程间通信!
问题1:一个进程通过网络将数据传输到远端主机上时,如何区分要把数据传给主机上的哪个进程呢?
实际上,每个进程都有属于自己的端口号(port)。IP地址用于标识主机的唯一性,而端口号则用于标识一个进程在该主机中的唯一性。所以网络通信中我们不止需要IP地址来找到目标主机,还需要端口号找到目标进程
端口号是传输层协议的内容,是一个2字节16位的整数,用于在主机中标识一个进程的唯一性。因此通过IP地址+端口号就能标识全网唯一的一个进程
问题2:进程PID也可以标识进程在主机中的唯一性,为什么还要有端口号?
- 不是所有的进程都需要进行网络通信,但是所有进程都要有自己的PID
- 将系统和网络功能解耦
一个端口号只能被一个进程绑定,但一个进程可以绑定多个端口号
二、初识TCP/UDP协议
传输层协议(TCP和UDP)的数据段中分别记录了源端口号和目的端口号,用来描述数据是从哪个进程发的、要发给哪个进程
关于TCP和UDP协议,我们首先对它们有一个简单且直观的认识,后续再进行深入了解
TCP(Transmission Control Protocol,传输控制协议):
- 面向连接
- 保证数据传输可靠性
- 面向字节流
UDP(User Datagram Protocol,用户数据报协议):
- 无连接
- 不保证数据传输可靠性
- 面向数据报
三、网络字节序
3.1 概念
内存中的多字节数据相对于内存地址而言有大端和小端的区别,因此主机也分为大端机和小端机
让我们回顾一下大端和小端的概念
大端:数据的高位存储在内存的低位
小端:数据的高位存储在内存的高位

不止是内存,网络数据流中同样有大端小端之分。发送方在发送数据时通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收方将数据保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
问题在于,不同类型的主机在跨网络互相传输数据时就可能导致问题。例如大端机将数据发送给小端机,就可能导致数据的错乱
因此TCP/IP协议规定,发送到网络中的数据流应统一按照大端字节序发送。也就是说不论是大端机还是小端机,都要按照TCP/IP规定的网络字节序来发送或接收数据
所以如果发送数据的主机是小端机,必须先将数据转换成大端字节序后再发送。在后面调用套接字相关API时,我们也通常需要对端口号和ip地址进行网络字节序转换。
3.2 常用API
为了让网络程序具有可移植性,我们可以使用下列库函数进行主机字节序和网络字节序的转换
cpp
#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就是将32位长整型从主机字节序转为网络字节序,适用于转换IP地址
如果主机字节序本身是小端,调用对应库函数后则会将参数做相应大小端转换后返回;如果主机字节序已经是大端了,则不作改变
四、Socket套接字
4.1 概念
套接字(Socket)是一种独立于协议的网络编程接口,是对网络中不同主机的应用进程之间进行双向通信的端点的抽象。套接字上联应用进程,下接网络协议栈,是应用程序与网络协议栈进行交互的接口。
套接字包括 IP 地址和端口号两个部分,可以用来区分不同的进程之间的数据传输。传输层使用的协议不同,套接字的种类也会发生相应的改变。
在Linux中,套接字的本质也是文件,因此有对应的网络文件描述符,用户通过网络文件描述符对套接字进行操作。
4.2 常用API
(1)socket
cpp
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket函数类似于打开文件的操作,会创建套接字并返回一个网络文件描述符,其中:
- domain:协议域,又称协议族,例如AF_INET代表IPv4协议,AF_INET6代表IPv6协议
- type:指定socket类型,例如流式套接字SOCK_STREAM(TCP)和数据报套接字SOCK_DGRAM(UDP)
- protocal:指定协议信息,常见的有IPPROTO_TCP、IPPROTO_UDP等,通常设置为0代表自动选择套接字类型对应的默认协议
创建成功返回一个网络文件描述符,失败返回-1并设置环境变量errno
例如:

(2)bind
cpp
#include <sys/types.h>
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
bind函数用于将一个服务的ip地址和端口号绑定到一个套接字上,一般是服务端在绑定监听套接字时会用到。客户端则不必要调用bind绑定,因为客户端的端口号由内核自动分配
其中:
- socket:待绑定的网络文件描述符
- address:指向一个sockaddr结构体的指针,该结构体包含了要绑定的ip地址和端口号
- address_len:address指向的结构体大小
成功绑定返回0, 失败返回-1并设置errno
例如:
cpp
uint16_t port = 8888; //端口号
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (sockfd < 0)
{
// 创建套接字失败时
//...
}
//填充结构体字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4协议
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定
{
//绑定失败时
//...
}
填充结构体字段时,需要对端口号进行网络字节序转换和对字符串格式的ip地址转四字节ip地址后再填充到sockaddr_in结构体中
关于ip地址的格式转换函数会在后面提及,这里先简单提一下sockaddr的结构
sockaddr结构
关于socket的API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等,但是各种网络协议的地址格式并不相同。
例如IPv4的地址用sockaddr_in结构体表示,其中包含16位地址类型、16位端口号和32位ip地址

不同的结构体中,前16位都填充了ip地址的协议类型,因此我们可以统一用struct sockaddr*类型接收,取得结构体首地址后按位数获取地址类型字段就可以确定是哪一种结构体了。
在使用Unix域套接字进行本机进程间通信时,绑定时就得使用sockaddr_un结构
(3)listen
cpp
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen函数常用于服务端监听来自客户端的TCP连接请求,通常在调用bind函数后使用,成功返回0,失败返回-1并设置errno
其中:
- sockfd:将被设置为监听状态的网络文件描述符
- backlog:设置全连接队列的长度(全连接队列用于临时维护未被上层accept的已经建立好的连接,长度为backlog+1)
例如:
cpp
uint16_t port = 8888; //端口号
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (sockfd < 0)
{
// 创建套接字失败时
//...
}
//填充结构体字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4协议
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定
{
//绑定失败时
//...
}
if (listen(sockfd, 10) < 0) // 将套接字设置为监听状态,全连接队列最多存放10+1个连接
{
//监听失败时
//...
}
(4)accept
cpp
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数常用于服务端从全连接队列中接收来自客户端的TCP连接请求并创建一个新的套接字,通常用于listen函数后。成功会返回该套接字的文件描述符用来负责后续的数据通信服务,失败返回-1并设置errno。
如果全连接队列中暂时没有Tcp连接请求,accept函数将阻塞等待直到有客户端发起连接请求(除非服务器处于非阻塞状态)
其中:
- sockfd:被绑定并设置为监听状态的套接字对应的文件描述符
- addr:指向sockaddr结构体的指针,用于填充客户端对应的地址信息。设置为NULL表示不关心客户端地址
- addrlen:指向socklen_t的指针,表示addr的大小
例如:
cpp
uint16_t port = 8888; //端口号
string ip = "127.0.0.1"; //字符串格式的ip地址
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
if (sockfd < 0)
{
// 创建套接字失败时
//...
}
//填充结构体字段
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; //IPv4协议
local.sin_port = htons(port);
inet_aton(ip.c_str(), &(local.sin_addr));
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定
{
// 绑定失败时
//...
}
if (listen(sockfd, 10) < 0) // 将套接字设置为监听状态,全连接队列最多存放10+1个连接
{
// 监听失败时
//...
}
struct sockaddr_in client; // 存储客户端信息的结构体
socklen_t len = sizeof(client);
int newfd = accept(sockfd, (struct sockaddr *)&client, &len); // sockfd只负责获取连接,newfd负责后续的数据通信服务
if (newfd < 0)
{
// 接收失败时
//...
}
(5)connect
cpp
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数常用于发起建立网络连接的请求,成功返回0,失败返回-1并设置errno
其中:
- sockfd:调用socket函数创建套接字成功后返回的文件描述符
- addr:指向sockaddr结构体的指针,其中包含了准备建立连接的目标服务器地址信息
- addrlen:addr指向的结构体的大小
例如:
cpp
string serverip = "127.0.0.1";
uint16_t serverport = 8888;
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sockfd < 0)
{
// 创建套接字失败时
//...
}
// 填充结构体字段
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
// 发起连接
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
// 连接发起失败时
//...
}
(6)recvfrom
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom常用于使用UDP协议(或其他无连接的数据报服务)时从套接字中读取数据,成功返回读取到的字节数,当套接字已经关闭时返回0,出错返回-1并设置errno
其中:
- sockfd:已打开的套接字文件描述符
- buf:指向用于存放接收到的数据的缓冲区的指针
- len:缓冲区大小
- flags:控制接收行为的标志,通常设置为0表示阻塞模式
- src_addr:指向一个sockaddr结构体,存储数据来源方的地址信息
- addrlen:代表sockaddr结构体的大小
例如:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if (sockfd < 0)
{
//...
}
char buffer[1024];
sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&temp, &len); // 接收服务端返回的消息
//...
(7)sendto
cpp
#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);
sendto函数常用于使用UDP协议时通过指定的socket将数据发送到目标主机,成功返回实际发送的字节数,失败返回-1并设置errno
其中:
- sockfd:已打开的套接字文件描述符
- buf:指向要发送的数据
- len:要发送的数据长度
- flags:标志位,通常设置为0
- dest_addr:指向存储目标主机地址信息的sockaddr结构体
- addrlen:结构体大小
4.3 地址转换函数
sockaddr_in结构体中的成员sin_addr表示32位的ip地址,但我们日常中见到的ip地址通常是点分十进制格式的字符串表示的。通过一些函数可以实现ip地址在两种格式间的转换。
字符串转32位ip地址:
(1)inet_aton
cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
其中:
- cp:待转换的点分十进制ip地址字符串
- inp:指向in_addr结构体的指针,存储转换后的网络字节序ip地址
in_addr内部存放了一个32位整型用于存储转换后的ip地址,其结构如下:
cpp
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
例如:
cpp
struct sockaddr_in addr;
inet_aton("127.0.0.1", &addr.sin_addr);
(2)inet_addr
cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
其中cp是待转换的点分十进制ip地址字符串
例如:
cpp
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
(3)inet_pton
cpp
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
其中:
- af:协议族
- src:指向点分十进制ip地址字符串的指针
- dst:指向用于存储转换后ip地址的内存区域
网络字节序ip地址转点分十进制的函数有inet_ntoa、inet_ntop,有兴趣的可以自行查阅文档
完.