文章目录
- 1.预备知识
- 1.1 源IP地址和目的IP地址
- 1.2 端口号
- 基本概念
- 作用
- 分类
- 知名端口(Well - Known Ports)
- 注册端口(Registered Ports)
- 动态或私有端口(Dynamic or Private Ports)
- 常见知名端口号
- 使用机制
- TCP协议中的端口使用
- UDP协议中的端口使用
- 1.3 网络字节序
- 基本概念
- 产生原因
- 保证数据一致性
- 简化网络编程
- 规范网络协议
- 促进网络互联
- 转换方法
- 3.socket编程
- 3.1 概念
- 3.2 工作原理
- 3.3 分类
- 按传输协议分类
- TCP Socket(流式套接字)
- UDP Socket(数据报套接字)
- 按地址族分类
- IPv4 Socket 编程
- IPv6 Socket 编程
- Unix 域 Socket 编程
- 3.4 socket 常见API
- 3.5 sockaddr
- 基本结构
- 常见变体
- `sockaddr_in`(用于 IPv4 地址)
- `sockaddr_in6`(用于 IPv6 地址)
- `sockaddr_un`(用于 Unix 域套接字)
- 使用方法
1.预备知识
1.1 源IP地址和目的IP地址
- 源 IP 地址:是指发送 IP 数据包的设备的 IP 地址,它标识了数据包的起始点。在一次网络通信中,源 IP 地址代表了发送数据的主机或网络设备,用于让接收方知道数据是从哪里来的。
- 目的 IP 地址:是指接收 IP 数据包的设备的 IP 地址,它标识了数据包的目标终点。目的 IP 地址告诉网络中的路由器和其他设备,数据包应该被发送到哪个具体的主机或网络。
1.2 端口号
端口号在计算机网络通信里是一个关键概念,它主要用于在同一台设备上区分不同的网络应用程序或服务。
基本概念
在网络通信中,IP地址用于标识网络中的设备,而端口号则用于标识设备上运行的具体应用程序或服务。端口号是一个16位的整数,取值范围为0 - 65535。当两台设备进行通信时,除了要知道对方的IP地址,还需要知道对应的端口号,才能准确地将数据发送到目标应用程序。
作用
- 区分应用程序:一台设备上可能同时运行多个网络应用程序,如浏览器、邮件客户端、文件传输工具等。通过端口号,操作系统可以将接收到的网络数据准确地分发到对应的应用程序。例如,当你在浏览器中访问网页时,浏览器会使用特定的端口号与网站服务器进行通信,操作系统根据端口号将服务器返回的数据交给浏览器处理。
- 实现多路复用:端口号使得多个应用程序可以同时使用同一个IP地址进行网络通信。在传输层协议(如TCP和UDP)的支持下,不同的应用程序可以使用不同的端口号来监听和发送数据,从而实现了网络资源的高效利用。
分类
知名端口(Well - Known Ports)
- 范围:0 - 1023。
- 特点:这些端口号被预先分配给一些常见的网络服务和应用程序,并且通常由系统管理员或特权用户使用。例如,HTTP服务默认使用端口80,HTTPS服务使用端口443,FTP服务使用端口20和21,SMTP服务使用端口25等。
注册端口(Registered Ports)
- 范围:1024 - 49151。
- 特点:这些端口号可以由用户或应用程序开发者向互联网号码分配机构(IANA)注册使用,用于一些特定的应用程序或服务。许多第三方应用程序会使用注册端口来提供特定的功能,如一些游戏、即时通讯软件等可能会使用注册端口进行通信。
动态或私有端口(Dynamic or Private Ports)
- 范围:49152 - 65535。
- 特点:这些端口号通常由操作系统动态分配给客户端应用程序使用。当客户端应用程序需要与服务器建立连接时,操作系统会从动态端口范围中选择一个空闲的端口号作为客户端的源端口号。连接结束后,该端口号会被释放,可供其他应用程序再次使用。
常见知名端口号
- 21端口:用于FTP(文件传输协议)的控制连接,客户端通过该端口与FTP服务器建立连接,并发送命令来控制文件的传输操作。
- 22端口:SSH(安全外壳协议)使用的端口,通过该端口可以在不安全的网络中建立安全的远程登录和文件传输通道。
- 25端口:SMTP(简单邮件传输协议)的默认端口,用于发送电子邮件。邮件客户端通过该端口将邮件发送到邮件服务器。
- 53端口:DNS(域名系统)服务使用的端口,用于域名与IP地址之间的解析。当你在浏览器中输入域名时,浏览器会通过该端口向DNS服务器查询对应的IP地址。
- 80端口:HTTP协议的默认端口,用于在互联网上传输超文本数据,即网页内容。当你访问一个普通的网站时,浏览器会默认使用该端口与网站服务器进行通信。
- 443端口:HTTPS协议的默认端口,是HTTP协议的安全版本,通过SSL/TLS加密技术对数据进行加密传输,保障数据的安全性和隐私性。
使用机制
TCP协议中的端口使用
- 服务器端:服务器应用程序会监听一个特定的知名端口号或注册端口号,等待客户端的连接请求。例如,Web服务器会监听80或443端口。当客户端发起连接请求时,服务器会在该端口上接收请求,并与客户端建立TCP连接。
- 客户端:客户端应用程序在发起连接时,会随机选择一个动态端口号作为源端口号,并指定服务器的IP地址和端口号作为目的地址和端口号。通过这种方式,客户端和服务器之间可以建立起一对一的通信连接。
UDP协议中的端口使用
- UDP是一种无连接的传输协议,使用端口号的方式与TCP类似。服务器和客户端都可以使用特定的端口号来发送和接收数据,但UDP不需要建立连接过程。例如,DNS查询通常使用UDP协议,客户端向DNS服务器的53端口发送查询请求,服务器接收到请求后,将查询结果返回给客户端的源端口号。
1.3 网络字节序
基本概念
字节序是指多字节数据在计算机内存中存储或在网络传输时各字节的存储顺序。
网络字节序规定在网络传输中,数据的高位字节在前,低位字节在后,也被称为大端字节序(Big - Endian)。
注意:网络数据发送过程中,先发出的数据是低地址,后发出的数据是高地址,即高位数据先发送,低位数据后发送。
产生原因
保证数据一致性
在网络通信中,数据需要从一个主机传输到另一个主机。为了确保接收方能够正确理解发送方发送的数据,需要一种统一的字节序标准。网络字节序规定数据在网络传输时采用大端字节序,这样无论发送方和接收方采用何种主机字节序,只要在发送数据时将主机字节序转换为网络字节序,在接收数据时将网络字节序转换为主机字节序,就可以保证数据的一致性。
简化网络编程
统一的网络字节序标准使得网络编程更加简单和规范。开发者在编写网络应用程序时,只需要遵循网络字节序的转换规则,而不需要考虑不同计算机系统的字节序差异。这样可以减少编程的复杂度,提高开发效率,同时也降低了出错的概率。
规范网络协议
各种网络协议(如 TCP/IP、HTTP、FTP 等)在定义数据格式时,都遵循网络字节序的标准。这是为了保证不同厂商生产的网络设备和不同开发者编写的网络应用程序之间能够相互兼容和通信。如果没有统一的网络字节序标准,网络协议的实现将变得非常复杂,不同系统之间的通信也会变得困难重重。
促进网络互联
网络的目的是实现不同计算机和设备之间的互联和通信。网络字节序作为一种统一的标准,有助于打破不同计算机系统之间的字节序障碍,促进全球范围内的网络互联和数据交换。无论是在局域网还是广域网中,网络字节序都确保了数据能够在不同的网络节点之间准确无误地传输。
转换方法
- 当网络字节序和主机字节序一致时:不需转换。
- 当网络字节序和主机字节序不一致时:
- 在发送数据时,需要将主机字节序(计算机系统本身采用的字节序)转换为网络字节序;
- 在接收数据时,需要将网络字节序转换为主机字节序。
在不同的编程语言和操作系统中,通常提供了相应的函数来进行字节序的转换:
- C语言:在UNIX和Linux系统中,提供了
htons
(主机字节序到网络字节序,用于16位数据)、htonl
(主机字节序到网络字节序,用于32位数据)、ntohs
(网络字节序到主机字节序,用于16位数据)和ntohl
(网络字节序到主机字节序,用于32位数据)等函数。
https://man.cx/htonl
示例:
#include <stdio.h>
#include <arpa/inet.h>int main() {uint16_t host_port = 8080;uint16_t net_port = htons(host_port);printf("Host port: %d, Network port: %d\n", host_port, net_port);return 0;
}
- Python:Python的
socket
模块提供了相关的转换函数,如socket.htons
和socket.htonl
用于主机到网络的转换,socket.ntohs
和socket.ntohl
用于网络到主机的转换。例如:
import sockethost_port = 8080
net_port = socket.htons(host_port)
print(f"Host port: {host_port}, Network port: {net_port}")
3.socket编程
3.1 概念
Socket(套接字)是一种网络编程接口,它提供了一种机制,使得运行在不同主机上的进程可以通过网络进行数据交换。Socket可以看作是网络中的一个端点,每个Socket都有一个唯一的地址,由IP地址和端口号组成。通过Socket,应用程序可以方便地发送和接收网络数据,而无需关心底层网络协议的细节。
3.2 工作原理
在Socket编程中,通常涉及客户端和服务器两个角色,它们的工作流程如下:
- 服务器端:首先创建一个Socket并绑定到指定的IP地址和端口号上,然后开始监听客户端的连接请求。当有客户端发起连接时,接受连接并创建一个新的Socket用于与客户端进行通信,之后就可以进行数据的收发操作,通信结束后关闭Socket。
- 客户端:创建一个Socket,指定服务器的IP地址和端口号,向服务器发起连接请求。连接成功后,就可以与服务器进行数据的收发,通信结束后关闭Socket。
3.3 分类
按传输协议分类
TCP Socket(流式套接字)
- 基于TCP(传输控制协议)实现,提供面向连接的、可靠的、字节流形式的数据传输服务。在数据传输前,需要先建立连接,传输过程中保证数据的顺序和完整性,传输结束后需要关闭连接。适用于对数据准确性要求较高的应用,如文件传输、网页浏览等。
UDP Socket(数据报套接字)
- 基于UDP(用户数据报协议)实现,提供无连接的、不可靠的数据传输服务。不需要建立连接,数据以独立的数据报形式发送,不保证数据的顺序和完整性,但具有较低的开销和较高的传输效率。适用于对实时性要求较高、对数据准确性要求相对较低的应用,如视频直播、实时游戏等。
按地址族分类
IPv4 Socket 编程
使用 IPv4 地址进行通信,IPv4 地址是 32 位的二进制数,通常以点分十进制的形式表示,如192.168.1.1。在 IPv4 Socket 编程中,需要指定 IPv4 地址和端口号来建立连接。大多数早期的网络应用都是基于 IPv4 Socket 编程实现的。
IPv6 Socket 编程
随着 IPv4 地址的枯竭,IPv6 逐渐得到广泛应用。IPv6 地址是 128 位的二进制数,采用冒号分十六进制的形式表示,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。IPv6 Socket 编程与 IPv4 类似,但需要使用 IPv6 地址和相应的套接字选项来支持 IPv6 协议。目前,越来越多的网络设备和应用开始支持 IPv6,以适应未来网络发展的需求。
Unix 域 Socket 编程
Unix 域套接字用于在同一台主机上的不同进程之间进行通信,它提供了一种高效的进程间通信(IPC)机制。Unix 域套接字可以使用流式套接字(类似 TCP)或数据报套接字(类似 UDP),但通信是在本地文件系统上进行的,而不是通过网络。与网络套接字相比,Unix 域套接字的通信速度更快,因为不需要经过网络协议栈的处理。常用于同一主机上不同进程之间的数据交换和同步,如数据库服务器和客户端进程之间的通信、图形界面程序和后台服务进程之间的通信等。
3.4 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);
3.5 sockaddr
在网络编程里,sockaddr
是一个结构体,属于通用的套接字地址结构,主要用于存储套接字的地址信息。
在进行 Socket 编程时,很多函数(像 bind()
、connect()
、accept()
等)都需要传入地址信息,而 sockaddr
就为这些不同类型的地址提供了统一的接口,以此来存储地址信息。
基本结构
在 C 语言中,sockaddr
结构体的定义一般如下:
#include <sys/socket.h>struct sockaddr {sa_family_t sa_family; /* 地址族,用于指定地址类型 */char sa_data[14]; /* 地址数据,存储具体的地址信息 */
};
sa_family
:这是一个sa_family_t
类型的成员,用来表明地址族,也就是地址的类型。常见的取值有AF_INET
(表示 IPv4 地址族)、AF_INET6
(表示 IPv6 地址族)、AF_UNIX
(表示 Unix 域套接字地址族)等。sa_data
:是一个长度为 14 字节的字符数组,用于存放具体的地址数据。不过由于它是通用的字节数组,使用起来不太方便,所以通常会使用针对不同地址族定义的特定结构体。
常见变体
sockaddr_in
(用于 IPv4 地址)
#include <netinet/in.h>struct sockaddr_in {sa_family_t sin_family; /* 地址族,固定为 AF_INET */in_port_t sin_port; /* 端口号,采用网络字节序 */struct in_addr sin_addr; /* IPv4 地址 */char sin_zero[8]; /* 填充字节,让结构体大小和 sockaddr 一致 */
};struct in_addr {uint32_t s_addr; /* IPv4 地址,以网络字节序存储 */
};
sockaddr_in6
(用于 IPv6 地址)
#include <netinet/in.h>struct sockaddr_in6 {sa_family_t sin6_family; /* 地址族,固定为 AF_INET6 */in_port_t sin6_port; /* 端口号,使用网络字节序 */uint32_t sin6_flowinfo; /* IPv6 流信息 */struct in6_addr sin6_addr; /* IPv6 地址 */uint32_t sin6_scope_id; /* 范围 ID */
};struct in6_addr {unsigned char s6_addr[16]; /* IPv6 地址 */
};
sockaddr_un
(用于 Unix 域套接字)
#include <sys/un.h>struct sockaddr_un {sa_family_t sun_family; /* 地址族,固定为 AF_UNIX */char sun_path[108]; /* Unix 域套接字的路径名 */
};
使用方法
在实际编程时,一般先使用特定的地址结构体(如 sockaddr_in
)来设置地址信息,然后再将其强制转换为 sockaddr
类型传递给相关函数。下面是一个简单的 TCP 服务器示例,展示了 sockaddr_in
和 sockaddr
的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 8888int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);// 创建套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定地址和端口,需要将 sockaddr_in 强制转换为 sockaddrif (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听连接if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 接受连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}// 关闭套接字close(new_socket);close(server_fd);return 0;
}
在这个例子中,先创建了 sockaddr_in
结构体变量 address
并设置地址信息,接着在调用 bind()
函数时,把 &address
强制转换为 (struct sockaddr *)&address
传递给函数,从而完成地址绑定操作。