秦皇岛市网站建设_网站建设公司_jQuery_seo优化
2026/1/10 3:47:08 网站建设 项目流程

源IP地址和⽬的IP地址

我们知道在⽹络中,IP ⽤来标识主机的唯⼀性。

源 IP 地址

  • 就是发送数据的设备的IP地址,相当于快递上的寄件人地址。

⽬的IP地址

  • 接收数据的设备的 IP 地址,相当于快递上的收件人地址。

端⼝号

端口号解决的是 “主机上哪个应用接收数据” 的问题。IP就相当于你们小区的地址,端口号port就代表了你在这一小区的哪一栋哪一户。

所以网络中通信的本质,实际上就是进程间通信。只不过该通信是跨越网络的,而我们将跨越网络的进程间通信称之为套接字通信。

认识端口号

  • 端⼝号是⼀个 2 字节 16 位的整数;
  • 端⼝号⽤来标识⼀个进程, 告诉操作系统, 当前的这个数据要交给哪⼀个进程来处理;
  • IP地址 + 端⼝号能够标识⽹络上的某⼀台主机的某⼀个进程;
  • ⼀个端⼝号只能被⼀个进程占⽤.

但是我们需要知道,端口号并不是自动分配给所有进程的,而是进行绑定和申请的。只有需要进行网络通信的进程,才需要分配端口号。

端口号的范围划分

0 - 1023 : 属于系统保留端口号,像HTTP, FTP, SSH 等这些⼴为使⽤的应⽤层协议, 他们的端⼝号都是固定的.(尽管有一些端口号还没有被正式分配,但也不推荐用来绑定普通用户进程,如若想绑定需要root权限。)

1024~49151 : 普通用户进程可直接绑定,无需管理员权限。

49152~65535:不建议服务器绑定,这个区间是操作系统的临时端口池,当客户端进程发起网络连接时,操作系统会从这个区间随机分配一个临时端口,作为客户端的源端口,通信结束后端口会被释放。

理解 "端⼝号" 和 "进程ID"

既然端口号(port)和 进程ID(PID)的作用都是标识一台主机上唯一的一个进程,那在进行socket通信时为什么不直接用进程ID 呢?

  • 我们知道PID是随进程的创建和销毁动态变化的,每重启一次服务器PID都会变化,这就会导致一个致命的问题,客户端每次访问都需要知道服务器进程的最新PID。

源端⼝号和⽬的端⼝号

传输层协议( TCP 和 UDP )的数据段中有两个端⼝号, 分别叫做源端⼝号和⽬的端⼝号. 就是在描述 "数据是谁发的, 要发给谁";

理解socket

  • IP代表互联网主机的唯一标识,Port= 主机内网络进程的唯一标识。
  • IP+Port= 套接字地址(Socket Address),能够代表互联网上的唯一进程了。
  • 通信的时候,本质是两个互联⽹进程代表⼈来进⾏通信,{srcIp,srcPort,dstIp,dstPort},这样的4元组就能标识互联⽹中唯⼆的两个进程。

TCP协议与UDP协议

如果我们了解了系统,也了解了⽹络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过⽹络协议栈进⾏通信,必定调⽤的是传输层提供的系统调⽤,来进⾏的⽹络通信。

认识UDP协议

特点:

  • 有连接 (TCP 通信前必须经过三次握手建立连接,通信后通过四次挥手关闭连接,连接的生命周期有明确的状态)
  • 可靠传输(TCP 通过序列号、确认应答(ACK)、重传机制保证数据的完整性。)
  • 面向字节流(TCP 把数据当作无边界的字节流处理,发送方可以分多次写数据,接收方可以分多次读数据)

他的这些特点( “可靠” 和 “有连接” )就注定了他有缺点:

  • 建立 / 关闭连接开销大
  • 协议头大,额外开销高
  • 实时性差
  • 状态维护成本高

认识UDP协议

特点:

  • 无连接,通信开销极低
  • 面向数据报,消息边界清晰
  • 不可靠传输,数据可能丢失 / 乱序 / 重复

UDP虽然有不可靠传输的特点,但并不代表它就比TCP要差。 一些优秀的网站在设计网络通信算法时,会同时采用TCP协议和UDP协议,当网络流畅时就使用UDP协议进行数据传输,而当网速不好时就使用TCP协议进行数据传输,此时就可以动态的调整后台数据通信的算法。

总结:编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。

⽹络字节序

我们知道,内存中的多字节数据相对于内存地址有⼤端和⼩端之分, 磁盘⽂件中的多字节数据相对于⽂件中的偏移地址也有⼤端⼩端之分, ⽹络数据流同样有⼤端⼩端之分.那么在网络中如何定义网络数据流的地址呢?

因此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);
  • 这几个函数名也很好理解,当中h 表⽰ host , n 表⽰ network , l 表⽰ (long) 32 位⻓整数, s 表⽰(short) 16 位短整数.
  • htonl 表⽰将 32 位的⻓整数从主机字节序转换为⽹络字节序后准备发送。
  • 如果主机是⼩端字节序,这些函数将参数做相应的⼤⼩端转换然后返回;
  • 如果主机是⼤端字节序,这些函数不做转换,将参数原封不动地返回。

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);

创建套接字socket

int socket(int domain, int type, int protocol);

作用:socket()的作用是向操作系统内核申请创建一个套接字(Socket)对象,并返回一个用于操作这个 Socket 的文件描述符(fd)。失败则返回-1。

参数:

domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位

  • 如果是本地通信就设置为AF_UNIX
  • 如果是网络通信就设置为AF_INET(IPv4)或 AF_INET6(IPv6)。

type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM

  • 如果是基于UDP 的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,
  • 如果是基于TCP 的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。

protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

绑定端口号bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()的核心是将创建好的 Socket 文件描述符(sockfd)与指定的套接字地址(IP+Port)关联起来。绑定成功返回0,绑定失败返回-1

参数:

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等
  • addrlen:传入的addr结构体的长度

发送数据send、sendto

// 用于面向连接(TCP)的套接字,将数据发送到已连接的对等方 ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 用于无连接(UDP)的套接字或未连接的面向连接套接字,将数据发送到指定地址 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • TCP 通信因为提前建立了连接(三次握手),内核已经知道目标地址,所以用send()直接发数据即可;
  • UDP 是无连接的,每次发数据都要告诉内核 “发给谁”,所以必须用sendto()指定目标IP+Port

成功时:返回实际发送的字节数。这个数字可能小于len,表示只发送了部分数据。这通常发生在以下情况:

  • send发送缓冲区已满,无法一次性发送全部数据,需要多次调用send函数将剩余数据发送完。

失败时:返回 -1

参数:

  • sockfd:套接字描述符。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送数据的长度。
  • flags:控制发送操作的标志,如 MSG_DONTWAIT (不阻塞)等。
  • dest_addr(仅 sendto):指向接收方地址的指针。
  • addrlen(仅 sendto):接收方地址结构体的长度。

接收数据 recv、recvfrom

// 用于面向连接的套接字,从已连接的对等方接收数据 ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 用于无连接的套接字或未连接的面向连接套接字,接收数据并获取发送方地址 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • TCP 因为提前建立了连接,内核已经知道数据来自哪个客户端,所以用recv()只需要接收数据即可。
  • UDP 是无连接的,每次收到数据都不知道是谁发的,所以必须用recvfrom()同时获取数据和发送方地址。

成功时:返回接收到的字节数,如果连接已经正常关闭,返回值为 0,表示对方已关闭连接。

失败时:返回 -1

参数:

  • sockfd:套接字描述符。
  • buf:指向接收数据的缓冲区的指针。
  • len:缓冲区的长度。
  • flags:控制接收操作的标志,如 MSG_DONTWAIT (不阻塞)等。
  • src_addr(仅 recvfrom):指向发送方地址的指针。
  • addrlen(仅 recvfrom):指向发送方地址结构体长度的指针。

监听套接字listen

int listen(int sockfd, int backlog);
  • 状态转换:把调用bind()后的 Socket 从主动套接字(可发起连接)转为被动监听套接字,告诉内核:“这个 Socket 要专门等待客户端的连接请求”;
  • 创建连接队列:内核会为这个监听 Socket 创建两个队列(未完成连接队列 + 已完成连接队列),用于存放 TCP 三次握手过程中的连接请求,backlog参数就是用来限制队列的最大长度。

成功:返回 0,表示套接字已成功进入监听状态,服务器可以接收客户端的连接请求。

失败:返回 -1,并设置相应的错误代码。可以使用perror函数来输出具体的错误信息。

参数:

  • sockfd:这是一个已经创建并绑定了本地地址的套接字描述符。套接字通常由socket函数创建,例如int sockfd = socket(AF_INET, SOCK_STREAM, 0);。
  • 对于AF_INET,表示使用 IPv4 协议族;SOCK_STREAM表示使用面向连接的 TCP 套接字。在调用listen之前,该套接字需要使用bind函数绑定到一个本地地址,以便让服务器程序在特定的地址和端口上监听客户端的连接请求。
  • backlog:它指定了连接请求队列的最大长度。当多个客户端同时尝试连接服务器时,操作系统会将这些连接请求存储在一个队列中等待服务器处理。
  • 例如,将backlog设置为 10,意味着最多可以有 10 个未处理的连接请求在队列中等待服务器的accept操作。设置这个值时需要权衡资源利用和性能,过小可能导致连接请求丢失,过大可能会占用过多的系统资源

接收请求accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 从连接队列取连接:从listen()创建的已完成连接队列中,取出一个三次握手完成的客户端连接;
  • 创建新 Socket:为这个客户端连接创建一个新的 Socket 文件描述符client_fd),后续和客户端的recv()/send()都通过这个新 fd 操作;
  • 获取客户端地址:可选地获取客户端的IP+Port(填充到addr参数中),方便服务器识别 “谁连过来了”。

返回值:

  • 成功:accept函数会返回一个新的套接字描述符,这个新的套接字将用于和客户端进行通信。服务器可以使用这个新的套接字进行读写操作,例如使用send和recv函数发送和接收数据。
  • 失败:如果accept调用失败,它将返回 -1,并设置相应的错误代码。可以使用perror函数来输出具体的错误信息,常见的错误可能包括系统资源不足、没有连接请求等待等。

参数:

sockfd:

  • 这是一个已经处于监听状态的套接字描述符,通常是由socket函数创建,并经过bind和listen函数处理后进入监听状态的套接字。例如,int sockfd = socket(AF_INET, SOCK_STREAM, 0);,并且经过listen(sockfd, backlog);处理。
  • 它代表服务器端的监听套接字,通过该套接字接收客户端的连接请求。

addr:

  • 这是一个指向struct sockaddr(或其变体,如struct sockaddr_in对于 IPv4)的指针,用于存储客户端的地址信息。
  • 当accept成功返回时,这个指针所指向的结构体将被填充为发起连接请求的客户端的地址信息,包括客户端的 IP 地址和端口号。
  • 如果不关心客户端的地址信息,可以将此参数设置为NULL。

addrlen:

  • 这是一个指向socklen_t类型的指针,它表示addr参数所指向的结构体的长度。
  • 在调用accept之前,需要将其初始化为addr所指向结构体的大小,例如,对于struct sockaddr_in,可以使用sizeof(struct sockaddr_in)。
  • 在accept返回后,addrlen的值可能会被修改为实际存储客户端地址信息的长度,尤其是在使用可变长度地址结构体时。

建立连接connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 发起连接请求:向指定的服务器IP+Port发送 TCP 三次握手的第一个 SYN 包;
  • 完成三次握手:等待服务器回复 SYN+ACK,再发送 ACK 包,最终建立 TCP 连接;
  • 但是UDP 是无连接的,调用connect()只是 “绑定默认目标地址”,不会发起任何网络包,后续可直接用send()代替sendto()

返回值:

成功:

  • 如果connect函数调用成功,它将返回 0,表示已经成功连接到服务器。一旦连接成功,客户端就可以使用这个套接字发送和接收数据,例如使用send和recv函数。

失败:

  • 如果connect函数调用失败,它将返回 -1,并设置相应的错误代码。可以使用perror函数输出具体的错误信息。常见的错误可能包括服务器不可达、连接超时、服务器未监听等。

参数:

  • sockfd:这是一个已经创建的客户端套接字描述符
  • addr:这是一个指向struct sockaddr(或其变体,如struct sockaddr_in对于 IPv4 或struct sockaddr_in6对于 IPv6)的指针,包含了服务器的地址信息。
  • addrlen:这是addr所指向的结构体的长度,通常使用sizeof操作符获取。

sockaddr结构

socket API是⼀层抽象的⽹络编程接⼝,适⽤于各种底层⽹络协议,如IPv4、IPv6,以及UNIX Domain Socket. 然⽽, 各种⽹络协议的地址格式并不相同.

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体:

  • sockaddr_in 结构体是用于跨网络通信的
  • sockaddr_un 结构体是用于本地通信的

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址⽤sockaddr_in结构体表⽰,包括16位地址类型, 16位端⼝号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的⾸地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都⽤struct sockaddr *类型表⽰, 在使⽤的时候需要强制转化成sockaddr_in; 这样的好处是程序的通⽤性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

为什么没有用 void* 代替 struct sockaddr* 类型

Socket API 诞生于 1980 年代的 BSD 系统,而 C 语言的void*是在 C89 标准(1989 年)才被标准化 —— 早期的 Socket API 设计时,void*还不是通用的 “无类型指针”,因此选择用struct sockaddr作为通用地址类型,后续为了兼容已有代码,一直保留了这个设计。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询