从零开始学习 Linux 内核套接字:掌握网络编程的必备技能

本文涉及的产品
数据传输服务 DTS,数据同步 small 3个月
推荐场景:
数据库上云
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 从零开始学习 Linux 内核套接字:掌握网络编程的必备技能

一、Socket相关概念

1.1套接字定义

套接字最早是由BSD(伯克利软件套件)在1982年引入的通信机制,目前已被广泛移植到主流的操作系统中。

对于应用开发人员来说,套接字(socket)是一个抽象层,是一种特殊的I/O接口,独立于具体协议的网络编程接口,也是一种文件描述符。应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。

套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。也就是说套接字是相对于网络通信来说的,网络套接字就是IP地址与端口的组合(套接字 = IP地址+TCP/UDP+端口号,套接字应该是成对的,本地(服务器)+ 外地(客户端))。

Socket是一种常用的进程之间通信机制,不仅能实现本地不同进程之间的通信,而且通过网络能够在不同主机的进程之间进行通信对于网络通信而言,每一个socket都可用网络地址结构{协议、本地地址、本地端口}来表示。Socket通过一个专门的函数创建,并返回一个整形的socket描述符。随后各种操作都是通过socket描述符来实现的。

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。

Socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。

套接字就是:一台主机的IP地址和端口号,套接字对就是互传信息两台主机的IP和端口号。在两台主机connect时,就是通过对应的套接字联系起来的。对客户来说:需要明确自己要连接的服务器IP和端口号,而自己的IP和端口号一般由内核默认了,会在连接后传给服务器。对服务器来说:需要明确自己监听的本机的端口就行,本机的IP可由宏INADDR_ANY经转换得到默认的IP给套接字结构。至于来自客户的IP和端口可以不用管,接收任何主机的连接。

我们所说的 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。

1.2简述

TCP/IP模型中的传输层实现端到端的通信,因此,每一个传输层连接有两个端点。那么,传输层连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口。传输层连接的端点叫做套接字(socket)。根据RFC793的定义:端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。

总之,套接字Socket =(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。

套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。

在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现。在Windows环境下,网络应用程序编程接口称作Windows Socket。为了支持用户开发面向应用的通信程序,大部分系统都提供了一组基于TCP或者UDP的应用程序编程接口(API),该接口通常以一组函数的形式出现,也称为套接字(Socket)。

1.3发展

Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口,也是在Internet上进行应用开发最为通用的API。

Windows系统流行起来之后,由Microsoft联合了其他几家公司在Berkeley Sockets的基础之上进行了扩充(主要是增加了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制),共同制定了一套Windows下的网络编程接口,即Windows Sockets规范。Windows Sockets规范是一套开放的、支持多种协议的Windows下的网络编程接口,包括1.1版和2.0版两个版本。其中1.1版只支持TCP/IP协议,而2.0版可以支持多协议,2.0版有良好的向后兼容性。当前Windows下的Internet软件绝大部分都是基于Windows Socks开发的。

1.4UNIX/Linux中的socket

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。

你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。请注意,网络连接也是一个文件,它也有文件描述符!

我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;
  • 用 write() 向远程计算机写入数据。
  • 只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!

1.5为什么需要socket

普通的I/O操作过程:打开文件>>>>>读/写文件>>>>>关闭文件,同一台主机上的两个进程可以通过管道、信号、共享内存等进行通信,那么两个不在同一台主机上的进程怎么进行通信呢?

TCP/IP协议被集成到操作系统的内核中,这就引入了新型的“I/O”操作,当两个不在一台主机上的进程要进行通信是,这时就需要socket

二、socket有哪些分类

套接字类型指的是套接字的数据传输方式,通过socket函数的第二个参数传递,只有这样才能决定创建的套接字的数据传输方式,套接字(socket)分为很多种,比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。这里只讲第一种套接字——Internet 套接字,它是最具代表性的,也是最经典最常用的。根据数据的传输方式,可以将 Internet 套接字分成两种类型,流式套接字和数据报套接字。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。 

2.1三种常用的套接字

为了满足不同的通信程序对通信质量和性能的要求,一般的网络系统提供了三种不同类型的套接字,以供用户在设计网络应用程序时根据不同的要求来选择。这三种套接为流式套接字(SOCK-STREAM)、数据报套接字(SOCK-DGRAM)和原始套接字(SOCK-RAW)。

(1)、流式套接字(SOCK_STREAM)-----面向连接的套接字

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。它提供了一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送,保证数据传输的可靠性和按序收发。如果数据损坏或丢失,可以重新发送。流格式套接字有自己的纠错机制。流式套接字内设流量控制,被传输的数据看作是无记录边界的字节流。在TCP/IP协议簇中,使用TCP协议来实现字节流的传输,当用户想要发送大批量的数据或者对数据传输有较高的要求时,可以使用流式套接字。

流失套接字特点:

  • 传输过程中数据不会丢失
  • 按序传输数据
  • 传输的数据不存在数据边界   

可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。

TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。

那么,“数据的发送和接收不同步”该如何理解呢?

假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。

流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性全部读取,也可能分成好几次读取。也就是说,在面向连接的套接字中,read函数和write函数的调用次数并无太大意义。所以说面向连接的套接字不存在数据边界。

也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。

那么接收数据的缓冲区被接收的数据填满会怎样,之后传递的数据是否会丢失呢?

首先调用read函数从缓冲区读取部分数据,因此,缓冲区并不总是满的。但如果read函数读取速度比接收数据的速度慢,则缓冲区有可能被填满。此时套接字无法再接收数据,但即使这样也不会丢失数据,因为这时传输端套接字将停止传输。也就是说,面向连接的套接字会根据接收端的状态进行数据传输,如果传输出错还会提供重传服务。因此,面向连接的套接字除特殊情况外不会发生数据丢失。

流格式套接字有什么实际的应用场景吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。

(2)、数据报套接字(SOCK_DGRAM)-----面向消息的套接字

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。它提供了一种无连接、不可靠的双向数据传输服务。数据通过相互独立的报文进行传输,是无序的,并且保留了记录边界,不提供可靠性保证。数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。在TCP/IP协议簇中,使用UDP协议来实现数据报套接字。在出现差错的可能性较小或允许部分传输出错的应用场合,可以使用数据报套接字进行数据传输,这样通信的效率较高。

因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。

可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 限制每次传输的数据大小
  • 数据的发送和接收是同步的(有的教程也称“存在数据边界”) 

面向消息的套接字比面向连接的套接字具有更快的传输速度,但无法避免数据丢失或损毁。另外,每次传输的数据大小具有一定限制,并存在数据边界。存在数据边界意味着接收数据的次数应和传输次数相同。总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求传输速度为目的的套接字。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

(3)、原始套接字(SOCK_RAM)

该套接字允许对较低层协议(如IP或ICMP)进行直接访问,它功能强大使用较为不便,常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网络设备。

2.2面向连接和无连接的套接字的区别

流格式套接字(Stream Sockets)就是“面向连接的套接字”,它基于 TCP 协议;数据报格式套接字(Datagram Sockets)就是“无连接的套接字”,它基于 UDP 协议。

这给大家造成一种印象,面向连接就是可靠的通信,无连接就是不可靠的通信,实际情况是这样吗?另外,不管是哪种数据传输方式,都得通过整个 Internet 网络的物理线路将数据传输过去,从这个层面理解,所有的 socket 都是有物理连接的呀,为什么还有无连接的 socket 呢?

从字面上理解,面向连接好像有一条管道,它连接发送端和接收端,数据包都通过这条管道来传输。当然,两台计算机在通信之前必须先搭建好管道。无连接好像没头苍蝇乱撞,数据包从发送端到接收端并没有固定的线路,爱怎么走就怎么走,只要能到达就行。每个数据包都比较自私,不和别人分享自己的线路,但是,大家最终都能殊途同归,到达接收端。

上图是一个简化的互联网模型,H1 ~ H6 表示计算机,A~E 表示路由器,发送端发送的数据必须经过路由器的转发才能到达接收端。

假设 H1 要发送若干个数据包给 H6,那么有多条路径可以选择,比如:

  • 路径①:H1 --> A --> C --> E --> H6
  • 路径②:H1 --> A --> B --> E --> H6
  • 路径③:H1 --> A --> B --> D --> E --> H6
  • 路径④:H1 --> A --> B --> C --> E --> H6
  • 路径⑤:H1 --> A --> C --> B --> D --> E --> H6

数据包的传输路径是路由器根据算法来计算出来的,算法会考虑很多因素,比如网络的拥堵状况、下一个路由器是否忙碌等。

(1)、无连接的套接字

对于无连接的套接字,每个数据包可以选择不同的路径,比如第一个数据包选择路径④,第二个数据包选择路径①,第三个数据包选择路径②……当然,它们也可以选择相同的路径,那也只不过是巧合而已。

每个数据包之间都是独立的,各走各的路,谁也不影响谁,除了迷路的或者发生意外的数据包,最后都能到达 H6。但是,到达的顺序是不确定的,比如:

第一个数据包选择了一条比较长的路径(比如路径⑤),第三个数据包选择了一条比较短的路径(比如路径①),虽然第一个数据包很早就出发了,但是走的路比较远,最终还是晚于第三个数据包达到。

第一个数据包选择了一条比较短的路径(比如路径①),第三个数据包选择了一条比较长的路径(比如路径⑤),按理说第一个数据包应该先到达,但是非常不幸,第一个数据包走的路比较拥堵,这条路上的数据量非常大,路由器处理得很慢,所以它还是晚于第三个数据包达到了。

还有一些意外情况会发生,比如:

第一个数据包选择了路径①,但是路由器C突然断电了,那它就到不了 H6 了。

第三个数据包选择了路径②,虽然路不远,但是太拥堵,以至于它等待的时间太长,路由器把它丢弃了。

总之,对于无连接的套接字,数据包在传输过程中会发生各种不测,也会发生各种奇迹。H1 只负责把数据包发出,至于它什么时候到达,先到达还是后到达,有没有成功到达,H1 都不管了;H6 也没有选择的权利,只能被动接收,收到什么算什么,爱用不用。

无连接套接字遵循的是「尽最大努力交付」的原则,就是尽力而为,实在做不到了也没办法。无连接套接字提供的没有质量保证的服务。

(2)、面向连接的套接字

面向连接的套接字在正式通信之前要先确定一条路径,没有特殊情况的话,以后就固定地使用这条路径来传递数据包了。当然,路径被破坏的话,比如某个路由器断电了,那么会重新建立路径。

这条路径是由路由器维护的,路径上的所有路由器都要存储该路径的信息(实际上只需要存储上游和下游的两个路由器的位置就行),所以路由器是有开销的。

H1 和 H6 通信完毕后,要断开连接,销毁路径,这个时候路由器也会把之前存储的路径信息擦除。

在很多网络通信教程中,这条预先建立好的路径被称为“虚电路”,就是一条虚拟的通信电路。

为了保证数据包准确、顺序地到达,发送端在发送数据包以后,必须得到接收端的确认才发送下一个数据包;如果数据包发出去了,一段时间以后仍然没有得到接收端的回应,那么发送端会重新再发送一次,直到得到接收端的回应。这样一来,发送端发送的所有数据包都能到达接收端,并且是按照顺序到达的。

发送端发送一个数据包,如何得到接收端的确认呢?很简单,为每一个数据包分配一个 ID,接收端接收到数据包以后,再给发送端返回一个数据包,告诉发送端我接收到了 ID 为 xxx 的数据包。

面向连接的套接字会比无连接的套接字多出很多数据包,因为发送端每发送一个数据包,接收端就会返回一个数据包。此外,建立连接和断开连接的过程也会传递很多数据包。

不但是数量多了,每个数据包也变大了:除了源端口和目的端口,面向连接的套接字还包括序号、确认信号、数据偏移、控制标志(通常说的 URG、ACK、PSH、RST、SYN、FIN)、窗口、校验和、紧急指针、选项等信息;而无连接的套接字则只包含长度和校验和信息。

有连接的数据包比无连接大很多,这意味着更大的负载和更大的带宽。许多即时聊天软件采用 UDP 协议(无连接套接字),与此有莫大的关系。

(3)、总结

两种套接字各有优缺点:

  • 无连接套接字传输效率高,但是不可靠,有丢失数据包、捣乱数据的风险;
  • 有连接套接字非常可靠,万无一失,但是传输效率低,耗费资源多。

两种套接字的特点决定了它们的应用场景,有些服务对可靠性要求比较高,必须数据包能够完整无误地送达,那就得选择有连接的套接字(TCP 服务),比如 HTTP、FTP 等;而另一些服务,并不需要那么高的可靠性,效率和实时才是它们所关心的,那就可以选择无连接的套接字(UDP 服务),比如 DNS、即时聊天工具等。

三、套接字接口

recvfrom()系统调用

  • 该函数把发送给程序的信息储存在缓冲区buffer中,并记录数据来源的程序IP端口,该函数为阻塞的
int recvfrom(int sockfd, void *buffer, size_t len,int flags, struct sockaddr *src_from, socklen_t *src_len);

buffer用于储存接收到的数据,len指定buffer的长度,而flags在应用中通常被设置0,src_from若不为空,则记录数据来源程序的IP端口,若src_len不为空,则其长度信息记录在src_len所指向的变量中。

sendto()系统调用

  • 该函数把缓冲区buffer中的信息给送给指定的IP端口的程序
int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);

buffer中储存着将要发送的数据,len是buffer的长度,而flags在应用中通常被设置为0,to是要发送数据到的程序的IP端口,tolen是to参数的长度。成功时返回发送的数据的字节数,失败时返回-1。

示例:server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
int main(int arc, char **argv)
{
    int server_sockfd = -1;
    socklen_t server_len = 0;
    socklen_t client_len = 0;
    char buffer[512];
    ssize_t result = 0;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    // 创建数据报套接字
    server_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    // 设置监听的端口、IP
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(9739);
    server_len = sizeof(server_addr);
    // 绑定(命名)套接字
    bind(server_sockfd, (struct sockaddr *)&server_addr, server_len);
    // 忽略子进程停止 或 退出 的信息,子进程成为僵尸进程后将由内核去管理
    signal(SIGCHLD, SIG_IGN);
    while (1)
    {
        // 接收数据,用 client_addr 来储存数据来源程序的IP端口
        // 函数会阻塞知道收到客户端发来的数据
        result = recvfrom(server_sockfd, buffer, sizeof(buffer), 0,
                          (struct sockaddr *)&client_addr, &client_len);
        if (fork() == 0)
        {
            // 利用子进程来处理数据
            buffer[0] += 'a' - 'A';
            // 发送处理后的数据
            sendto(server_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, client_len);
            printf("%c\n", buffer[0]);
            // 注意,一定要关闭子进程,否则程序运行会不正常
            exit(0);
        }
    }
    // 关闭套接字
    close(server_sockfd);
}

示例:client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char **argv)
{
    struct sockaddr_in server_addr;
    socklen_t server_len = 0;
    int sockfd = -1;
    char c = 'A';
    // 取第一个参数的第一个字符
    if (argc > 1)
    {
        c = argv[1][0];
    }
    // 创建数据报套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    // 设置服务器IP、端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(9739);
    server_len = sizeof(server_addr);
    // 向服务器发送数据
    sendto(sockfd, &c, sizeof(char), 0,
           (struct sockaddr *)&server_addr, server_len);
    // 接收服务器处理后发送过来的数据,由于不关心数据来源,所以把后两个参数设为0
    recvfrom(sockfd, &c, sizeof(char), 0, 0, 0);
    printf("char from server = %c\n", c);
    // 关闭套接字
    close(sockfd);
    exit(0);
}

如何使用 Socket?

以Linux为例:

/****server***/
// 1.建立套接字
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2.绑定端口号
bind(fd, (sockaddr*)&sockaddr_in, socklen_t);// bind函数用于将套接字文件描述符和网络端口号绑定起来,其第二个参数是含ip+port的一个结构体
// 3.监听
listen(fd, backlog);  // backlog表示同一时刻的客户端最大连接数
// 4.接收
int cli_fd = accpet(fd, (sockaddr*)&sockaddr_in, &socklen_t);   //返回一个客户端套接字描述符,后面两个参数是输出型参数,用于填充客户端的ip+port,**阻塞等待接收客户端连接到来**
// 5.给客户端发送数据
send(cli_fd, msg, msg_len, flag); // 最后一个参数为标志信息,一般为0
// 6.关闭套接字
close(fd);  
close(cli_fd);


/****client***/
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2.连接服务器
connect(fd, (sockaddr*)&sockaddr_in, socklen_t);
// 3.接收数据
recv(fd, buf, buf_size, flags);   // 0表示阻塞接收,如果返回值<0,则报错返回,如果返回值=0,表示接收到消息为空
// 4.关闭套接字
close(fd);

client 运行后,通过 connect() 函数向 server 发起请求,处于监听状态的 server 被激活,执行 accept() 函数,接受客户端的请求,然后执行 send() 函数向 client 传回数据。client 接收到传回的数据后,connect() 就运行结束了,然后使用 recv() 将数据读取出来。

四、分配给套接字的IP地址、MAC和端口号

IP是internet protocol(网络协议)的简写,是为收发网络数据而分配给计算机的值。端口并非赋予计算机的值,而是为区分程序中创建的套接字而分配给套接字的序号。

在茫茫的互联网海洋中,要找到一台计算机非常不容易,有三个要素必须具备,它们分别是 IP 地址、MAC 地址和端口号。

IP地址是 Internet Protocol Address 的缩写,译为“网际协议地址”。为使计算机连接到网络并收发数据,必须向其分配IP地址。IP地址分为两类:

  • IPv4(Internet Protocol version 4):4字节地址族
  • IPv6(Internet Protocol version 6):16字节地址族

目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。一台计算机可以拥有一个独立的 IP 地址,一个局域网也可以拥有一个独立的 IP 地址(对外就好像只有一台计算机)。对于目前广泛使用 IPv4 地址,它的资源是非常有限的,一台计算机一个 IP 地址是不现实的,往往是一个局域网才拥有一个 IP 地址。

在因特网上进行通信时,必须要知道对方的 IP 地址。实际上数据包中已经附带了 IP 地址,把数据包发送给路由器以后,路由器会根据 IP 地址找到对方的地里位置,完成一次数据的传递。路由器有非常高效和智能的算法,很快就会找到目标计算机。

3.1IP地址的作用

IP地址用来标识网络中的一台主机。根据不同的协议版本,分为IPv4(32位)和IPv6(128位)。

IPV4和IPV6的差别主要是表示IP地址所用的字节数,目前通用的地址族为IPV4。IPV6是为了应对2010年前后IP地址耗尽的问题而提出的标准,即便如此,现在还主要使用IPV4,IPV6的普及将需要更长时间。

一个IP地址主要包含两部分:网络号和主机号。网络号表示其属于互联网的哪一个局域网络,主机号表示其属于该局域网络中的哪一台主机。二者是主从关系。其中,网络号和主机号根据子网掩码来区分。简单的说,有了源IP和目标IP,数据包就能在不同的主机之间传输。

网络地址(网络ID)是为区分网络而设置的一部分IP地址。假设向http://WWW.SEMI.COM公司传输数据,该公司内部构建了局域网,把所有计算机连起来。因此,首先应向http://SEMI.COM网络传输数据,也就是说,并非一开始就浏览所有4字节IP地址,进而找到目标主机;而是仅浏览4字节IP地址的网络地址,先把数据送到http://SEMI.COM网络,http://SEMI.COM网络(构成网络的路由器)接收到数据后,浏览传输数据的主机地址(主机ID)并将数据传给目标主机。下图展示了数据传输过程:

某主机向203.211.172.103和203.211.217.202传输数据,其中203.211.172和203.211.217为该网络的网络地址。所以,“向相应网络传输数据”实际上是向构成网络的路由器(Route)或交换机(Switch)传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。

若想构建网络,需要一种物理设备完成外网与本网主机之间的数据交换,这种设备便是路由器或交换机。它们实际上也是一种计算机,只不过是为特殊目的而设计运行的,因此有了别名。所以,如果在我们使用的计算机上安装适当的软件,也可以将其作为交换机。另外,交换机比路由器功能要简单一些,但实际用途差别不大。

IPv4地址的分类

IP地址根据网络号和主机号来分,分为A、B、C三类及特殊地址D、E。

  • A类:(1.0.0.0-126.0.0.0)(默认子网掩码:255.0.0.0或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类IP地址的最前面为“0”,所以地址的网络号取值于1~126之间。一般用于大型网络。地址范围1.0.0.1~126.255.255.254。
  • B类:(128.0.0.0-191.255.0.0)(默认子网掩码:255.255.0.0或0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。一般用于中等规模网络。地址范围128.0.0.1~191.254.255.254。
  • C类:(192.0.0.0-223.255.255.0)(子网掩码:255.255.255.0或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。一般用于小型网络。地址范围192.0.1.1~223.255.254.254。
  • D类:组播地址。该类IP地址的最前面为“1110”,所以地址的网络号取值于224~239之间。一般用于多路广播用户,每一个组播地址代表一个多播组 。地址范围224.0.0.1~239.255.255.254。
  • E类:保留地址。该类IP地址的最前面为“1111”,所以地址的网络号取值于240~255之间。

在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:

  • A类地址:10.0.0.0~10.255.255.255
  • B类地址:172.16.0.0~172.31.255.255
  • C类地址:192.168.0.0~192.168.255.255

回送地址:127.0.0.1。 也是本机地址,等效于localhost或本机IP。一般用于测试使用。例如:ping 127.0.0.1来测试本机TCP/IP是否正常。

地址数据结构体

struct sockaddr
{
    unsigned short sa_family;       /*地址族*/
    char sa_data[14];               /*IP地址和端口号*/
}
struct sockaddr_in
{
    short int sin_family;           /*地址族*/
    struct in_addr sin_addr;        /*IP地址*/
    unsigned short int sin_port;    /*端口号*/
    unsigned char sin_zero[8];      /*不使用,一般用0填充,以保持与struct sockaddr同样大小*/
}
struct in_addr
{
    in_addr_t s_addr;               /*IP地址*/
}
struct sockaddr_in6
{
    sa_family_t sin6_family;       /*地址类型,取值为AF_INET6*/
    in_port_t sin6_port;           /*16位端口号*/
    uint32_t sin6_flowinfo;        /*IPv6流信息*/
    struct in6_addr sin6_addr;     /*具体的IPv6地址*/
    uint32_t sin6_scope_id;        /*接口范围ID*/
};

uint16_t、in_addr_t等类型可以参考POSIX(可移植操作系统接口),POSIX是为UNIX系列操作系统设立的标准,它定义了一些其它的数据类型,如下表所示:

//POSIX中定义的数据类型int8_t        signed 8-bit int                           带符号8位整数            <sys/types.h>
uint8_t       unsigned 8-bit int(unsigned char)          无符号8位整数            <sys/types.h>
int16_t       signed 16-bit                              int带符号16位整数        <sys/types.h>
uint16_t      unsigned 16-bit int(unsigned short)        无符号16位整数           <sys/types.h>
int32_t       signed 32-bit                              带符号32未整数           <sys/types.h>
uint32_t      unsigned 32-bit int(unsigned int)          无符号32位整数           <sys/types.h>
sa_family_t   地址族(address family)                     套接字地址结构的协议簇     <sys/socket.h>
socklen_t     长度(length of struct)                     套接字地址结构的长度       <sys/socket.h>
in_addr_t     IP地址,声明为uint32_t                       IPV4地址                <netinet/in.h>
in_port_t     端口号,声明为uint16_t                       TCP/UDP端口             <netinet/in.h>

这两个数据类型大小相同,通常用sockaddr_in来保存某个网络地址,在使用时强转成sockaddr类型的指针。

IP地址格式转换

IP地址有两种不同的格式:十进制点分形式和32位二进制形式。前者是用户所熟悉的形式,而后者则是网络传输中IP地址的存储方式。

IPv4地址转换函数有inet_addr()、inet_aton()、inet_ntoa(),而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。由于IPv6是下一代互联网的标准协议,因此这里讲解主要以IPv4为主。

inet_addr()和inet_pton()函数是将十进制点分形式转换成二进制形式,而inet_ntop()是将二进制地址形式转换为十进制点分形式。

在VS2013以后的版本中,增加了inet_pton()、inet_ntop()之类的新函数,用于IP地址在“点分十进制”和“二进制整数”之间转换,并且能够处理ipv4和ipv6。而inet_addr是老函数,高版本VS在编译时默认使用了新函数,所以会报该错误。

问题的解决:

  • (1)、用inet_pton新函数代替inet_addr函数。
  • (2)、修改VS配置,告诉它我就要旧函数,修改方法:项目->属性->C/C++->常规->SDL检查,将“是”改为“否”,即可。

3.2、MAC地址

现实的情况是,一个局域网往往才能拥有一个独立的 IP;换句话说,IP 地址只能定位到一个局域网,无法定位到具体的一台计算机。这可怎么办呀?这样也没法通信啊。

其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。

MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。

3.3、端口号

有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序(即哪个进程)来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。

端口号是一个无符号短整形(unsigned short int),取值范围0到65535。端口号是系统的一种资源,0~1023一般被系统程序所使用,1~255之间为众所周知的端口,256~1023端口通常由UNIX系统占用,1024~49151位已登记端口,49152~65535为动态或私有端口。TCP端口号和UDP端口号独立,互不影响。建议使用>=50000的端口。

如果说IP地址可以用来表示网络中的一台主机,那么端口号可以用来表示主机内部的某个套接字。换句话说,当一个套接字创建好后,需要把它和某个IP地址及端口号绑定,这样双方才能实现端到端通信。

IP用于区分计算机,只要有IP地址就能找到目标主机,但仅凭这些无法传输给目标主机中的应用程序,毕竟处理数据靠的还是目标主机中的程序。假设用户在上网的同时,一边欣赏视频,一边浏览网页,这里至少需要两个套接字,一个接收视频数据,一个接收网页数据,那么问题来了,怎么区分这两个套接字呢?或者说,怎么区分到达的数据是正在观看的视频,还是正在浏览的网页呢?这里就需要用到端口号了

计算机中一般配有NIC(Network Interface Card,网络接口卡)数据传输设备。通过NIC向计算机内部传输数据时会用到IP,操作系统负责把传递到内部的数据适配给套接字,这时就要利用端口号了。也就是说,通过NIC接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字,如图1-3所示:

图1-3 数据分配过程

端口号就是同一操作系统内区分不同套接字而设置的,因此无法将一个端口号分配给不同套接字。另外,端口号由16位构成,可分配的端口号范围是0~65535。但0~1023是知名端口(Well-known PORT),一般分配给特定应用程序,所以应当分配此范围之外的端口。另外,虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。例如:如果某TCP套接字使用8500号端口,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用

总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的目的应用程序

五、字节序

字节序又称为主机字节序,是指计算机中多字节整形数据的存储方式。字节序有两种:大端(低地址存高位字节,高地址存低位字节)和小端(低字节存低位字节,高地址存高位字节),在网络通信中,发送方和接收方有可能使用不同的字节序,为了保证数据接收后能正确的解析处理,统一规定:数据以大端字节序在网络上传输。因此数据在发送前和接收后都需要在主机字节序和网络字节序之间转换。

5.1函数说明

字节序转换涉及4个函数:htons()、ntohs()、htonl()和ntohl()。这里的h代表host,n代表network,s代表short,l代表long,l代表long。通常16bit的IP端口号用前两个函数处理,而IP地址用后两个函数来转换。调用这些函数只是使其得到相应的字节序,用户不需要知道该系统的主机字节序和网络字节序是否真正相等。如果相同不需要转换的话,该系统的这些函数会定义成空宏。

5.2函数格式

精选文章推荐阅读:


相关文章
|
6天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
32 4
|
10天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
31 6
|
16天前
|
存储 安全 Linux
|
18天前
|
Linux Shell 数据安全/隐私保护
|
9天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
31 9
|
8天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
28 6
|
9天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
28 5
|
9天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
10天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
9天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。