上一篇 --- 网络基础概念 [ 下 ]https://blog.csdn.net/Small_entreprene/article/details/147320155?fromshareblogdetailsharetypeblogdetailsharerId147320155sharereferPCsharesourceSmall_entreprenesharefromfrom_link理解源 IP 地址和目的 IP 地址在我们当前的认识中IP地址用于标识网络中的唯一主机后续我们会进一步对IP地址进行分类讨论。但需要注意的是数据到达主机并不是通信的终点因为数据最终是为用户服务的。比如聊天是人在交流下载是人在获取文件浏览网页是人在查看内容。那么用户是如何看到聊天消息、执行下载任务或浏览网页信息的呢实际上这些操作都是通过启动具体的应用程序来实现的比如QQ、迅雷、浏览器等。而这些正在运行中的应用程序实例在操作系统层面就是一个个进程。可以说进程是用户活动在系统中的代表——只要数据被正确地交付给目标进程用户就相当于“拿到”了数据。因此数据送达主机只是一个中间步骤真正的目标是将数据递送到主机内部、交给指定的进程处理。从用户行为的角度来看我们上网可以归纳为两种基本操作从远程服务器获取数据例如刷抖音就是将视频内容从服务器端拉取到手机端将本地数据上传到远程服务器例如登录时提交账号密码或上传文件到百度网盘。无论我们的上网行为多么丰富多样从技术实现上都可以归结为这两种类型下载接收和上传发送。在具体的数据流动过程中数据由进程生成或消费进程运行在内存中。当发生网络通信时数据需要经过网卡才能发送到网络或者从网络中接收。整个路径可以简化为进程 ↔ 操作系统网络栈 ↔ 网卡 ↔ 网络而网卡正是连接主机与外部网络的硬件接口。网络和网卡之间以及网卡和进程之间共同协作完成了数据的收发过程。我们将上述过程中进程与网卡之间的数据交互称为I/O输入/输出操作。从冯·诺依曼体系结构的角度看网卡作为外部设备只能通过 I/O 接口与主机进行数据交换。这种硬件层面的限制也决定了上层应用软件的行为模式本质上应用程序所能做的就是从网络中获取信息接收数据和向网络中发送信息输出数据。从这个意义上来讲网络通信的本质就是位于不同主机上的两个进程之间进行数据交互。而如果进一步深究这种跨主机的进程间通信其底层依然遵循进程间通信的一般规律——即不同进程需要能够访问到同一份共享资源。那么在不同主机之间这个“共享资源”是什么呢就是网络本身。网络充当了连接两端进程的媒介使得数据可以在它们之间流通。所以我们可以这样理解网络通信只不过是把原本在同一台主机内部进行的进程间通信扩展到了两台不同主机之间而已。现在我们已经明确数据送达目标主机只是一个中间步骤真正目的是把数据交给主机内部某个具体的进程。然而在真实的系统中同一时刻可能运行着大量进程而到达主机的数据包究竟该交给哪一个进程呢哪些数据属于哪个进程必须要有明确的规定和机制。因此当数据包到达目标主机后系统必须能够根据某种规则准确地将数据分发给对应的进程。这就需要我们在网络体系结构中不仅要能唯一地标识一台主机通过 IP 地址还需要进一步在主机内部标识出接收数据的特定进程。这种标识和分发机制是保证数据能够“从网络到进程”准确交付的关键也是后续网络协议设计中要解决的核心问题之一。当主机从网络中接收到来自远端设备的各种数据包后操作系统需要将这些数据准确地分发给运行在系统内部的对应进程。然而一台主机上往往同时运行着成百上千个进程哪些数据应该交给哪个进程必须有明确的判别依据。为此系统必须提供一种机制在主机内部唯一地标识出接收数据的进程而不仅仅是在整个网络中标识主机本身这已由IP地址完成。为了解决这一需求在网络体系结构中我们引入了新的概念——端口号Port Number。简单来说IP地址用于在网络中唯一标识一台主机而端口号则用于在主机内部唯一标识一个具体的进程或服务。通过“IP地址 端口号”的组合我们就可以在全网范围内唯一地确定一个通信端点从而确保数据能够从源主机准确无误地送达目标主机上的目标进程。认识端口号端口号Port是传输层协议中的核心概念它用一个2字节16位的整数来表示取值范围为0~65535。端口号的作用是在主机内部唯一标识一个进程它告诉操作系统当前接收到的数据应当交付给哪一个进程来处理。具体来说当我们编写一个网络服务程序如QQ服务器或客户端时该进程在启动过程中需要通过操作系统提供的系统调用如bind将自身与一个指定的端口号进行绑定。这样当传输层从网络报文中提取出目的端口号后操作系统就能根据该端口号找到对应的进程并交付数据。通过IP地址 端口号的组合我们可以在全网范围内唯一地标识一台主机上的某一个进程。其中IP地址负责标识主机端口号负责标识该主机上的进程。关于端口号与进程的占用关系需要明确两点一个端口号在同一个时刻只能被一个进程占用这保证了端口号作为进程标识的唯一性避免了数据分发时的歧义。反过来一个进程可以同时绑定多个端口号因为系统只要求“从端口号能唯一查找到进程”这一方向成立即可并不限制一个进程占用多个端口。从通信的本质上讲网络通信就是全网范围内两个进程之间进行的数据交互我们通过“对方IP 对方端口号”来唯一标识通信的对端。在计算机网络中我们将IP地址和端口号的组合称为Socket套接字它是网络通信中连接应用层与传输层的关键抽象也是进程间进行网络通信的端点。不过端口号可以用来标识系统中唯一的一个网络进程但是我们学习过pid 也是进程的唯一标识那为什么不直接用进程pid呢不是所有的进程都需要进行网络通信不过我们从技术角度上来说使用 pid 不使用端口号这是可行的但是 pid 是一个系统的概念如果未来 pid 这个概念变化了伴随着网络就需要变这就是耦合性差使用端口号就可以进行解耦端口号的范围划分是【065535】因为是一个两字节16比特位的整数0 - 1023知名端口号HTTP、FTP、SSH 等这些广为使用的应用层协议它们的端口号都是固定的。1024 - 65535操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的。传输层协议TCP 和 UDP的数据段中有两个端口号分别叫做源端口号和目的端口号就是在描述【数据是哪台主机的哪一个进程发的要发给哪台主机上的哪一个进程】。理解socket总结一下IP地址用于在互联网中唯一标识一台主机端口号Port用于在主机内部唯一标识一个网络进程两者组合起来——即IP地址 端口号——便能在互联网中唯一地确定一个通信端点也就是一个进程。因此网络通信的实质就是两个位于不同主机上的进程在代表用户进行数据交互。我们可以通过一个四元组{源IP源端口目标IP目标端口}来完整描述一次通信的双方在全网范围内唯一标识出这两个进程。这也再次印证了网络通信归根结底还是进程间通信。关于“套接字Socket”这一概念的澄清在日常教学中我们常把IP Port称为套接字Socket这种说法便于初学者理解。但从计算机专业的角度来看更严谨的定义是套接字地址Socket Address指IP地址 端口号这个二元组用于在网络中唯一标识一个通信端点即一个进程。套接字Socket则是操作系统提供的一种编程接口抽象它是应用程序访问网络协议栈的入口可以理解为通信链路的一端。一个完整的网络连接需要由两端的套接字共同确定具体包括本端套接字本机 IP 本机端口对端套接字对端 IP 对端端口。简单来说Socket Address 是“地址”Socket 是“操作这个地址的接口/端点”。在实际编码中我们通过 Socket 接口来绑定 Socket Address从而完成通信链路的建立与数据传输。从本质上讲网络通信就是数据交互而数据交互在操作系统层面体现为 I/O 操作。在 Unix/Linux 系统中一切 I/O 都被抽象为“文件”操作因此网络通信也不例外——Socket 本质上就是一个文件描述符file descriptor。具体来说当我们创建一个 Socket 时操作系统会返回一个非负整数这就是文件描述符。应用程序可以通过读写这个文件描述符像操作普通文件一样向网络中发送数据或从网络中接收数据。从这个角度看Socket 就是一个进程“接收/发送数据的开口”它把网络通信的能力以文件接口的形式暴露给了应用程序。至于四元组 {源IP源端口目标IP目标端口}它是在协议栈的底层主要是传输层被维护和使用的。操作系统内核在管理每一个 Socket 时都会在内核数据结构中记录与之关联的四元组信息用于标识这条通信链路的两端。当数据包到达网卡后协议栈会根据包中的四元组信息找到对应的 Socket 结构进而将数据交付给持有该文件描述符的进程。所以整个链路可以这样串联起来网卡收到数据 → 协议栈根据四元组查找对应 Socket → 找到对应的文件描述符 → 进程通过 read/recv 等接口读取数据这也再次印证了网络通信底层就是文件 I/O而 Socket 就是进程与网络之间的桥梁。传输层的典型代表如果我们了解了系统也了解了网络协议栈我们就会清楚传输层是属于内核的。那么我们要通过网络协议栈进行通信必定调用的是传输层提供的系统调用来进行网络通信。传输层有两个重要协议TCP 和 UDP。认识 TCP 协议此处我们先对 TCPTransmission Control Protocol 传输控制协议有一个直观的认识后面我们再详细讨论 TCP 的一些细节问题。传输层协议有连接在数据传输之前需要先建立连接。打电话你喂我喂的过程就是建立连接的过程可靠传输保证数据的完整性和顺序性通过确认和重传机制确保数据可靠传输。面向字节流数据以字节流的形式传输不保留消息边界。自来水怎么接接多少都是自己自主决定的文件打开也叫文件流字节流和文件流没有区别都是流式的学习完自定义协议后我们就能理解了认识 UDP 协议此处我们也是对 UDPUser Datagram Protocol 用户数据报协议有一个直观的认识后面再详细讨论。传输层协议无连接不需要建立连接直接发送数据。对讲机不可靠传输不保证数据的完整性和顺序性数据可能丢失或乱序到达。面向数据报数据以数据报的形式传输保留消息边界。发快递发几个就只能几个TCP是可靠的丢包了可以再发但是UDP不可靠那为什么还要保留UDP呢属于同层协议但是还保留一个不可靠的我们要注意这里的可靠和不可靠不可以将其视为贬义词而是一种中性词是一种特点。TCP 保证可靠性意味着他一定要做更多的工作也就是意味着 TCP 协议会更加复杂一些复杂带来的就是占有资源会比较多。UDP 协议就会很简单简单的话就是代表开发周期短可维护性好。因为我们暂时还没有深入了解 TCP 和 UDP 协议此处只做了解即可。网络字节序大端我们以前学过计算机在存储数据的时候是有大端和小端的大小端是按照字节为单位的。大端Big-Endian定义大端字节序是指高位字节存放在内存的低地址端低位字节存放在内存的高地址端。假设有一个32位的整数0x12345678在大端字节序下它在内存中的存储顺序如下内存地址 0x00 0x01 0x02 0x03 存储内容 12 34 56 780x12存储在最低地址0x000x78存储在最高地址0x03小端Little-Endian小-小-小定义小端字节序是指低位字节存放在内存的低地址端高位字节存放在内存的高地址端。假设有一个32位的整数0x12345678在小端字节序下它在内存中的存储顺序如下内存地址 0x00 0x01 0x02 0x03 存储内容 78 56 34 120x78低权值位就是16的几次方存储在最低地址0x000x12存储在最高地址0x03#include stdio.h // 函数将整数从当前字节序转换为网络字节序大端 unsigned int htonl(unsigned int hostlong) { unsigned char *bytes (unsigned char*)hostlong; return ((unsigned int)(bytes[0] 24) | (bytes[1] 16) | (bytes[2] 8) | bytes[3]); } int main() { unsigned int x 1; if (*((char *)x) 0) { printf(大端Big-Endian\n); } else { printf(小端Little-Endian\n); printf(转换前的值%u\n, x); // 调用 htonl 函数将整数转换为大端字节序 x htonl(x); printf(转换后大端的值%u\n, x); } return 0; }如果今天主机 A 是小端存储主机 B 是大端存储两台主机间要进行网络通信A 将数据发送给 B 的话主机 B 就解释反了。所以在网络当中两台主机如果主机间的存储序列不同的话经过网络通信会导致对方将接收到的数据解释错了所以在网络中就有规定凡是将数据发送到网络当中的话一定是要按照大端的形式发送发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中也是按内存地址从低到高的顺序保存。因此网络数据流的地址应这样规定先发出的数据是低地址后发出的数据是高地址。TCP/IP 协议规定网络数据流应采用大端字节序即低地址高字节。不管这台主机是大端机还是小端机都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端就需要先将数据转成大端否则就忽略直接发送即可。为使网络程序具有可移植性使同样的 C 代码在大端和小端计算机上编译后都能正常运行可以调用以下库函数做网络字节序和主机字节序的转换。htonl把主机序的 32 位整数转成网络序hhosttonnetworkllonghtons把主机序的 16 位整数转成网络序sshortntohl把网络序的 32 位整数转成主机序ntohs把网络序的 16 位整数转成主机序16 位占 2 个字节1 字节 8 位能表示的整数范围大概是 -3276832767带符号比如端口号065535就是 16 位无符号整数所以用htons/ntohs处理32 位占 4 个字节能表示的整数范围大概是 -21 亿21 亿带符号比如 IP 地址IPv4本质是 32 位整数所以用htonl/ntohl处理。这些函数名很好记h 表示 hostn 表示 networkl 表示 32 位长整数s 表示 16 位短整数。简化记忆例如htonl表示将 32 位的长整数从主机字节序转换为网络字节序例如将 IP 地址转换后准备发送。如果主机是小端字节序这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节序这些函数不做转换将参数原封不动地返回。注意所有发送到网络上的数据都必须是大端的socket 编程接口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);这些 API 的详细细节我们会在后续写代码的时候进行说明。我们发现大部分的接口参数中都有 const struct sockaddr* 的结构体指针其他暂时不关心下面我们来聊一聊这个 sockaddr 这个结构体。sockaddr 结构体我们现在已经明确网络通信的本质就是跨主机的进程间通信。在此之前我们学习过的System V 标准如共享内存、消息队列、信号量主要用于同一台主机内部的进程间通信。而后续我们将要接触的POSIX 标准则是一套更现代、更通用的进程间通信标准它既支持本地通信也天然支持网络通信。这也是为什么在实际开发中System V 版本的进程间通信方式逐渐被边缘化——因为 POSIX 标准不仅涵盖了本地通信的需求还能无缝扩展到网络场景用一套机制解决了更多问题。在通信方式上Socket套接字根据应用场景的不同分为多种类型网络 SocketNetwork Socket用于跨主机的网络通信本地 SocketUnix Domain Socket用于同一台主机内的进程间通信原始 SocketRaw Socket用于更底层的网络操作通常属于系统编程范畴我们在常规开发中一般无需关心。需要强调的是只要我们学懂了网络 Socket本地 Socket 的理解就会变得水到渠成因为它们在设计思路上高度一致。然而面对不同场景通常需要提供不同的接口规范网络一套、本地一套。但 Socket 接口的设计者并不希望这样——他们希望只提供一套统一的通信接口既能用于网络通信也能用于本地通信从而降低学习成本和使用复杂度。为了达到这一目的就需要对接口进行统一抽象。在底层设计中引入了一个关键的结构体 ——sockaddr结构体。不同场景使用不同的具体结构体如网络用sockaddr_in本地用sockaddr_un但在接口层面上统一使用sockaddr *类型进行参数传递从而实现了“一套接口多种场景”的目标。这正应和了计算机中的一条通用设计哲学“没有什么问题是多加上一层抽象解决不了的。”通用性struct sockaddr提供了一个通用的接口适用于各种网络协议。专用性struct sockaddr_in和struct sockaddr_un分别针对 IPv4 网络通信和本地通信进行了优化提供了必要的信息和灵活性。在网络编程中sockaddr结构体及其派生结构体如sockaddr_in用于 IPv4sockaddr_in6用于 IPv6sockaddr_un用于 UNIX 域套接字之间经常需要进行强制类型转换。这并非偶然而是源于这套接口的通用设计哲学。为什么要这样设计sockaddr是一个通用的地址结构体它定义了一套统一的接口规范目的是为了支持多种不同的网络协议和地址类型。而sockaddr_in、sockaddr_un等则是针对特定协议的具体实现它们各自包含协议独有的地址信息。在调用系统提供的网络接口时如bind()、connect()、accept()等这些函数的参数类型被统一设计为struct sockaddr *。因此当你传入的是sockaddr_in *或sockaddr_un *时就需要显式地进行强制类型转换将其转为struct sockaddr *。为什么能这样做关键在于sockaddr结构体的第一个字段是sa_family_t sa_family用于表示地址族类型如AF_INET表示 IPv4AF_UNIX表示本地通信。而所有派生结构体如sockaddr_in在布局上都以这个地址族字段开头。因此内核或系统函数在接收到struct sockaddr *指针后会先读取sa_family字段从而判断出具体的地址类型然后根据类型去解析后续字段。这是一种典型的“通用头部 特定负载”的设计模式。为什么不用void *你可能会问既然要支持任意类型为什么不直接把参数设计成void *原因有两方面语义不足void *虽然可以指向任意类型的数据但它不携带任何类型信息。函数收到void *后无法获知该如何解析后面的数据也无法校验传入的地址结构是否合法。而sockaddr *通过sa_family字段提供了必要的类型标识使函数能够根据该字段正确解析和处理地址数据。历史原因这套套接字 API 最早在1983 年的 4.2 BSD UNIX 中发布远早于1989 年的 ANSI C 标准。在早期的 KR C 中根本没有void *类型。因此设计者只能选择用一个明确的指针类型即struct sockaddr *作为统一参数并要求调用者进行强制转换。这一设计被保留至今成为网络编程中的惯例。从面向对象的角度理解如果我们用面向对象的视角来看这套机制本质上就是用 C 语言模拟了“继承”和“多态”sockaddr扮演“基类”角色定义了公共的“接口规范”和类型标识sa_familysockaddr_in、sockaddr_un等扮演“派生类”角色在公共头部的基础上扩展了各自特有的地址信息函数接口如bind接收sockaddr *参数并在运行时根据sa_family判断具体类型并执行不同处理这就是“多态”的体现。通过这种设计C 语言在缺乏面向对象语法支持的情况下依然实现了一套高复用、可扩展的网络编程接口。而强制类型转换正是这套“手工多态”机制在使用时的必然表现形式。
socket 编程基础
上一篇 --- 网络基础概念 [ 下 ]https://blog.csdn.net/Small_entreprene/article/details/147320155?fromshareblogdetailsharetypeblogdetailsharerId147320155sharereferPCsharesourceSmall_entreprenesharefromfrom_link理解源 IP 地址和目的 IP 地址在我们当前的认识中IP地址用于标识网络中的唯一主机后续我们会进一步对IP地址进行分类讨论。但需要注意的是数据到达主机并不是通信的终点因为数据最终是为用户服务的。比如聊天是人在交流下载是人在获取文件浏览网页是人在查看内容。那么用户是如何看到聊天消息、执行下载任务或浏览网页信息的呢实际上这些操作都是通过启动具体的应用程序来实现的比如QQ、迅雷、浏览器等。而这些正在运行中的应用程序实例在操作系统层面就是一个个进程。可以说进程是用户活动在系统中的代表——只要数据被正确地交付给目标进程用户就相当于“拿到”了数据。因此数据送达主机只是一个中间步骤真正的目标是将数据递送到主机内部、交给指定的进程处理。从用户行为的角度来看我们上网可以归纳为两种基本操作从远程服务器获取数据例如刷抖音就是将视频内容从服务器端拉取到手机端将本地数据上传到远程服务器例如登录时提交账号密码或上传文件到百度网盘。无论我们的上网行为多么丰富多样从技术实现上都可以归结为这两种类型下载接收和上传发送。在具体的数据流动过程中数据由进程生成或消费进程运行在内存中。当发生网络通信时数据需要经过网卡才能发送到网络或者从网络中接收。整个路径可以简化为进程 ↔ 操作系统网络栈 ↔ 网卡 ↔ 网络而网卡正是连接主机与外部网络的硬件接口。网络和网卡之间以及网卡和进程之间共同协作完成了数据的收发过程。我们将上述过程中进程与网卡之间的数据交互称为I/O输入/输出操作。从冯·诺依曼体系结构的角度看网卡作为外部设备只能通过 I/O 接口与主机进行数据交换。这种硬件层面的限制也决定了上层应用软件的行为模式本质上应用程序所能做的就是从网络中获取信息接收数据和向网络中发送信息输出数据。从这个意义上来讲网络通信的本质就是位于不同主机上的两个进程之间进行数据交互。而如果进一步深究这种跨主机的进程间通信其底层依然遵循进程间通信的一般规律——即不同进程需要能够访问到同一份共享资源。那么在不同主机之间这个“共享资源”是什么呢就是网络本身。网络充当了连接两端进程的媒介使得数据可以在它们之间流通。所以我们可以这样理解网络通信只不过是把原本在同一台主机内部进行的进程间通信扩展到了两台不同主机之间而已。现在我们已经明确数据送达目标主机只是一个中间步骤真正目的是把数据交给主机内部某个具体的进程。然而在真实的系统中同一时刻可能运行着大量进程而到达主机的数据包究竟该交给哪一个进程呢哪些数据属于哪个进程必须要有明确的规定和机制。因此当数据包到达目标主机后系统必须能够根据某种规则准确地将数据分发给对应的进程。这就需要我们在网络体系结构中不仅要能唯一地标识一台主机通过 IP 地址还需要进一步在主机内部标识出接收数据的特定进程。这种标识和分发机制是保证数据能够“从网络到进程”准确交付的关键也是后续网络协议设计中要解决的核心问题之一。当主机从网络中接收到来自远端设备的各种数据包后操作系统需要将这些数据准确地分发给运行在系统内部的对应进程。然而一台主机上往往同时运行着成百上千个进程哪些数据应该交给哪个进程必须有明确的判别依据。为此系统必须提供一种机制在主机内部唯一地标识出接收数据的进程而不仅仅是在整个网络中标识主机本身这已由IP地址完成。为了解决这一需求在网络体系结构中我们引入了新的概念——端口号Port Number。简单来说IP地址用于在网络中唯一标识一台主机而端口号则用于在主机内部唯一标识一个具体的进程或服务。通过“IP地址 端口号”的组合我们就可以在全网范围内唯一地确定一个通信端点从而确保数据能够从源主机准确无误地送达目标主机上的目标进程。认识端口号端口号Port是传输层协议中的核心概念它用一个2字节16位的整数来表示取值范围为0~65535。端口号的作用是在主机内部唯一标识一个进程它告诉操作系统当前接收到的数据应当交付给哪一个进程来处理。具体来说当我们编写一个网络服务程序如QQ服务器或客户端时该进程在启动过程中需要通过操作系统提供的系统调用如bind将自身与一个指定的端口号进行绑定。这样当传输层从网络报文中提取出目的端口号后操作系统就能根据该端口号找到对应的进程并交付数据。通过IP地址 端口号的组合我们可以在全网范围内唯一地标识一台主机上的某一个进程。其中IP地址负责标识主机端口号负责标识该主机上的进程。关于端口号与进程的占用关系需要明确两点一个端口号在同一个时刻只能被一个进程占用这保证了端口号作为进程标识的唯一性避免了数据分发时的歧义。反过来一个进程可以同时绑定多个端口号因为系统只要求“从端口号能唯一查找到进程”这一方向成立即可并不限制一个进程占用多个端口。从通信的本质上讲网络通信就是全网范围内两个进程之间进行的数据交互我们通过“对方IP 对方端口号”来唯一标识通信的对端。在计算机网络中我们将IP地址和端口号的组合称为Socket套接字它是网络通信中连接应用层与传输层的关键抽象也是进程间进行网络通信的端点。不过端口号可以用来标识系统中唯一的一个网络进程但是我们学习过pid 也是进程的唯一标识那为什么不直接用进程pid呢不是所有的进程都需要进行网络通信不过我们从技术角度上来说使用 pid 不使用端口号这是可行的但是 pid 是一个系统的概念如果未来 pid 这个概念变化了伴随着网络就需要变这就是耦合性差使用端口号就可以进行解耦端口号的范围划分是【065535】因为是一个两字节16比特位的整数0 - 1023知名端口号HTTP、FTP、SSH 等这些广为使用的应用层协议它们的端口号都是固定的。1024 - 65535操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的。传输层协议TCP 和 UDP的数据段中有两个端口号分别叫做源端口号和目的端口号就是在描述【数据是哪台主机的哪一个进程发的要发给哪台主机上的哪一个进程】。理解socket总结一下IP地址用于在互联网中唯一标识一台主机端口号Port用于在主机内部唯一标识一个网络进程两者组合起来——即IP地址 端口号——便能在互联网中唯一地确定一个通信端点也就是一个进程。因此网络通信的实质就是两个位于不同主机上的进程在代表用户进行数据交互。我们可以通过一个四元组{源IP源端口目标IP目标端口}来完整描述一次通信的双方在全网范围内唯一标识出这两个进程。这也再次印证了网络通信归根结底还是进程间通信。关于“套接字Socket”这一概念的澄清在日常教学中我们常把IP Port称为套接字Socket这种说法便于初学者理解。但从计算机专业的角度来看更严谨的定义是套接字地址Socket Address指IP地址 端口号这个二元组用于在网络中唯一标识一个通信端点即一个进程。套接字Socket则是操作系统提供的一种编程接口抽象它是应用程序访问网络协议栈的入口可以理解为通信链路的一端。一个完整的网络连接需要由两端的套接字共同确定具体包括本端套接字本机 IP 本机端口对端套接字对端 IP 对端端口。简单来说Socket Address 是“地址”Socket 是“操作这个地址的接口/端点”。在实际编码中我们通过 Socket 接口来绑定 Socket Address从而完成通信链路的建立与数据传输。从本质上讲网络通信就是数据交互而数据交互在操作系统层面体现为 I/O 操作。在 Unix/Linux 系统中一切 I/O 都被抽象为“文件”操作因此网络通信也不例外——Socket 本质上就是一个文件描述符file descriptor。具体来说当我们创建一个 Socket 时操作系统会返回一个非负整数这就是文件描述符。应用程序可以通过读写这个文件描述符像操作普通文件一样向网络中发送数据或从网络中接收数据。从这个角度看Socket 就是一个进程“接收/发送数据的开口”它把网络通信的能力以文件接口的形式暴露给了应用程序。至于四元组 {源IP源端口目标IP目标端口}它是在协议栈的底层主要是传输层被维护和使用的。操作系统内核在管理每一个 Socket 时都会在内核数据结构中记录与之关联的四元组信息用于标识这条通信链路的两端。当数据包到达网卡后协议栈会根据包中的四元组信息找到对应的 Socket 结构进而将数据交付给持有该文件描述符的进程。所以整个链路可以这样串联起来网卡收到数据 → 协议栈根据四元组查找对应 Socket → 找到对应的文件描述符 → 进程通过 read/recv 等接口读取数据这也再次印证了网络通信底层就是文件 I/O而 Socket 就是进程与网络之间的桥梁。传输层的典型代表如果我们了解了系统也了解了网络协议栈我们就会清楚传输层是属于内核的。那么我们要通过网络协议栈进行通信必定调用的是传输层提供的系统调用来进行网络通信。传输层有两个重要协议TCP 和 UDP。认识 TCP 协议此处我们先对 TCPTransmission Control Protocol 传输控制协议有一个直观的认识后面我们再详细讨论 TCP 的一些细节问题。传输层协议有连接在数据传输之前需要先建立连接。打电话你喂我喂的过程就是建立连接的过程可靠传输保证数据的完整性和顺序性通过确认和重传机制确保数据可靠传输。面向字节流数据以字节流的形式传输不保留消息边界。自来水怎么接接多少都是自己自主决定的文件打开也叫文件流字节流和文件流没有区别都是流式的学习完自定义协议后我们就能理解了认识 UDP 协议此处我们也是对 UDPUser Datagram Protocol 用户数据报协议有一个直观的认识后面再详细讨论。传输层协议无连接不需要建立连接直接发送数据。对讲机不可靠传输不保证数据的完整性和顺序性数据可能丢失或乱序到达。面向数据报数据以数据报的形式传输保留消息边界。发快递发几个就只能几个TCP是可靠的丢包了可以再发但是UDP不可靠那为什么还要保留UDP呢属于同层协议但是还保留一个不可靠的我们要注意这里的可靠和不可靠不可以将其视为贬义词而是一种中性词是一种特点。TCP 保证可靠性意味着他一定要做更多的工作也就是意味着 TCP 协议会更加复杂一些复杂带来的就是占有资源会比较多。UDP 协议就会很简单简单的话就是代表开发周期短可维护性好。因为我们暂时还没有深入了解 TCP 和 UDP 协议此处只做了解即可。网络字节序大端我们以前学过计算机在存储数据的时候是有大端和小端的大小端是按照字节为单位的。大端Big-Endian定义大端字节序是指高位字节存放在内存的低地址端低位字节存放在内存的高地址端。假设有一个32位的整数0x12345678在大端字节序下它在内存中的存储顺序如下内存地址 0x00 0x01 0x02 0x03 存储内容 12 34 56 780x12存储在最低地址0x000x78存储在最高地址0x03小端Little-Endian小-小-小定义小端字节序是指低位字节存放在内存的低地址端高位字节存放在内存的高地址端。假设有一个32位的整数0x12345678在小端字节序下它在内存中的存储顺序如下内存地址 0x00 0x01 0x02 0x03 存储内容 78 56 34 120x78低权值位就是16的几次方存储在最低地址0x000x12存储在最高地址0x03#include stdio.h // 函数将整数从当前字节序转换为网络字节序大端 unsigned int htonl(unsigned int hostlong) { unsigned char *bytes (unsigned char*)hostlong; return ((unsigned int)(bytes[0] 24) | (bytes[1] 16) | (bytes[2] 8) | bytes[3]); } int main() { unsigned int x 1; if (*((char *)x) 0) { printf(大端Big-Endian\n); } else { printf(小端Little-Endian\n); printf(转换前的值%u\n, x); // 调用 htonl 函数将整数转换为大端字节序 x htonl(x); printf(转换后大端的值%u\n, x); } return 0; }如果今天主机 A 是小端存储主机 B 是大端存储两台主机间要进行网络通信A 将数据发送给 B 的话主机 B 就解释反了。所以在网络当中两台主机如果主机间的存储序列不同的话经过网络通信会导致对方将接收到的数据解释错了所以在网络中就有规定凡是将数据发送到网络当中的话一定是要按照大端的形式发送发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中也是按内存地址从低到高的顺序保存。因此网络数据流的地址应这样规定先发出的数据是低地址后发出的数据是高地址。TCP/IP 协议规定网络数据流应采用大端字节序即低地址高字节。不管这台主机是大端机还是小端机都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端就需要先将数据转成大端否则就忽略直接发送即可。为使网络程序具有可移植性使同样的 C 代码在大端和小端计算机上编译后都能正常运行可以调用以下库函数做网络字节序和主机字节序的转换。htonl把主机序的 32 位整数转成网络序hhosttonnetworkllonghtons把主机序的 16 位整数转成网络序sshortntohl把网络序的 32 位整数转成主机序ntohs把网络序的 16 位整数转成主机序16 位占 2 个字节1 字节 8 位能表示的整数范围大概是 -3276832767带符号比如端口号065535就是 16 位无符号整数所以用htons/ntohs处理32 位占 4 个字节能表示的整数范围大概是 -21 亿21 亿带符号比如 IP 地址IPv4本质是 32 位整数所以用htonl/ntohl处理。这些函数名很好记h 表示 hostn 表示 networkl 表示 32 位长整数s 表示 16 位短整数。简化记忆例如htonl表示将 32 位的长整数从主机字节序转换为网络字节序例如将 IP 地址转换后准备发送。如果主机是小端字节序这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节序这些函数不做转换将参数原封不动地返回。注意所有发送到网络上的数据都必须是大端的socket 编程接口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);这些 API 的详细细节我们会在后续写代码的时候进行说明。我们发现大部分的接口参数中都有 const struct sockaddr* 的结构体指针其他暂时不关心下面我们来聊一聊这个 sockaddr 这个结构体。sockaddr 结构体我们现在已经明确网络通信的本质就是跨主机的进程间通信。在此之前我们学习过的System V 标准如共享内存、消息队列、信号量主要用于同一台主机内部的进程间通信。而后续我们将要接触的POSIX 标准则是一套更现代、更通用的进程间通信标准它既支持本地通信也天然支持网络通信。这也是为什么在实际开发中System V 版本的进程间通信方式逐渐被边缘化——因为 POSIX 标准不仅涵盖了本地通信的需求还能无缝扩展到网络场景用一套机制解决了更多问题。在通信方式上Socket套接字根据应用场景的不同分为多种类型网络 SocketNetwork Socket用于跨主机的网络通信本地 SocketUnix Domain Socket用于同一台主机内的进程间通信原始 SocketRaw Socket用于更底层的网络操作通常属于系统编程范畴我们在常规开发中一般无需关心。需要强调的是只要我们学懂了网络 Socket本地 Socket 的理解就会变得水到渠成因为它们在设计思路上高度一致。然而面对不同场景通常需要提供不同的接口规范网络一套、本地一套。但 Socket 接口的设计者并不希望这样——他们希望只提供一套统一的通信接口既能用于网络通信也能用于本地通信从而降低学习成本和使用复杂度。为了达到这一目的就需要对接口进行统一抽象。在底层设计中引入了一个关键的结构体 ——sockaddr结构体。不同场景使用不同的具体结构体如网络用sockaddr_in本地用sockaddr_un但在接口层面上统一使用sockaddr *类型进行参数传递从而实现了“一套接口多种场景”的目标。这正应和了计算机中的一条通用设计哲学“没有什么问题是多加上一层抽象解决不了的。”通用性struct sockaddr提供了一个通用的接口适用于各种网络协议。专用性struct sockaddr_in和struct sockaddr_un分别针对 IPv4 网络通信和本地通信进行了优化提供了必要的信息和灵活性。在网络编程中sockaddr结构体及其派生结构体如sockaddr_in用于 IPv4sockaddr_in6用于 IPv6sockaddr_un用于 UNIX 域套接字之间经常需要进行强制类型转换。这并非偶然而是源于这套接口的通用设计哲学。为什么要这样设计sockaddr是一个通用的地址结构体它定义了一套统一的接口规范目的是为了支持多种不同的网络协议和地址类型。而sockaddr_in、sockaddr_un等则是针对特定协议的具体实现它们各自包含协议独有的地址信息。在调用系统提供的网络接口时如bind()、connect()、accept()等这些函数的参数类型被统一设计为struct sockaddr *。因此当你传入的是sockaddr_in *或sockaddr_un *时就需要显式地进行强制类型转换将其转为struct sockaddr *。为什么能这样做关键在于sockaddr结构体的第一个字段是sa_family_t sa_family用于表示地址族类型如AF_INET表示 IPv4AF_UNIX表示本地通信。而所有派生结构体如sockaddr_in在布局上都以这个地址族字段开头。因此内核或系统函数在接收到struct sockaddr *指针后会先读取sa_family字段从而判断出具体的地址类型然后根据类型去解析后续字段。这是一种典型的“通用头部 特定负载”的设计模式。为什么不用void *你可能会问既然要支持任意类型为什么不直接把参数设计成void *原因有两方面语义不足void *虽然可以指向任意类型的数据但它不携带任何类型信息。函数收到void *后无法获知该如何解析后面的数据也无法校验传入的地址结构是否合法。而sockaddr *通过sa_family字段提供了必要的类型标识使函数能够根据该字段正确解析和处理地址数据。历史原因这套套接字 API 最早在1983 年的 4.2 BSD UNIX 中发布远早于1989 年的 ANSI C 标准。在早期的 KR C 中根本没有void *类型。因此设计者只能选择用一个明确的指针类型即struct sockaddr *作为统一参数并要求调用者进行强制转换。这一设计被保留至今成为网络编程中的惯例。从面向对象的角度理解如果我们用面向对象的视角来看这套机制本质上就是用 C 语言模拟了“继承”和“多态”sockaddr扮演“基类”角色定义了公共的“接口规范”和类型标识sa_familysockaddr_in、sockaddr_un等扮演“派生类”角色在公共头部的基础上扩展了各自特有的地址信息函数接口如bind接收sockaddr *参数并在运行时根据sa_family判断具体类型并执行不同处理这就是“多态”的体现。通过这种设计C 语言在缺乏面向对象语法支持的情况下依然实现了一套高复用、可扩展的网络编程接口。而强制类型转换正是这套“手工多态”机制在使用时的必然表现形式。