LV06-03-网络编程-02-scoket编程接口
本文主要是网络编程——socket编程接口的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
| Windows | windows11 |
| Ubuntu | Ubuntu16.04的64位版本 |
| VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
| SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
| 开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
| uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
| linux内核 | linux-4.15(NXP官方提供) |
| STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
| 参考方向 | 参考原文 |
| --- | --- |
一、socket编程接口
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
①、调用 socket()函数打开套接字;
②、调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
③、调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
④、调用 accept()函数处理到来的连接请求。
接下来就来学习一下这些函数。
1. socket()函数
1.1 函数说明
在linux下可以使用man 2 socket命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于创建一个网络通信端点(打开一个网络通信),也就是创建一个套接字用于网络通信。
【函数参数】
domain:int类型,用于选择将用于通信的协议族,对于TCP/IP协议来说,通常选择AF_INET就可以了,当然如果我们的IP协议的版本支持IPv6,那么也可以选择AF_INET6。
点击查看 domain 常用可取的值及含义
| domain | 说明 | 帮助页 |
| AF_UNIX | Local communication | unix(7) |
| AF_LOCAL | ||
| AF_INET | IPv4 Internet protocols | ip(7) |
| AF_INET6 | IPv6 Internet protocols | ipv6(7) |
| AF_IPX | IPX - Novell protocols | |
| AF_NETLINK | Kernel user interface device | netlink(7) |
| AF_X25 | ITU-T X.25 / ISO-8208 protocol | x25(7) |
| AF_AX25 | Amateur radio AX.25 protocol | ax25(4) |
| AF_PACKET | Low-level packet interface | packet(7) |
| AF_ALG | Interface to kernel crypto API | |
| AF_APPLETALK | AppleTalk | ddp(7) |
type:int类型,用于指定套接字的类型。常用的有SOCK_STREAM(流格式套接字/面向连接的套接字) 和SOCK_DGRAM(数据报套接字/无连接的套接字)。
点击查看 type 常见取值及含义
| type | 说明 |
| SOCK_STREAM | 流式套接字,提供有序的、可靠的、双向的、基于连接的字节流,能保证数据正确传送到对方,用于TCP协议;可以支持带外数据传输机制。 |
| SOCK_DGRAM | 数据报套接字固定长度的、无连接的、不可靠的报文传递,用于UDP协议。 |
| SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递。 |
| SOCK_RAW | 原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少。 |
| SOCK_RDM | 提供不保证排序的可靠数据报层。 |
| SOCK_PACKET | 已过时,不应在应用程序中使用。 |
protocol:int类型,表示传输协议,该参数通常设置为0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特定协议。在AF_INET通信域中,套接字类型为SOCK_STREAM的默认协议是传输控制协议(Transmission Control Protocol,TCP协议),套接字类型为SOCK_DGRAM的默认协议是UDP。
【返回值】int类型,成功返回一个文件描述符,该描述符一般被称为称为socket描述符,该文件描述符将会用于网络通信;失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | socket_fd = 3 |
2. bind()函数
2.1 函数说明
在linux下可以使用man 2 bind命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于将一个IP地址和端口号与一个套接字进行绑定(将套接字与地址进行关联)。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址(注意这里说的地址包括IP地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的IP地址以及对应的端口号,所以通常服务器的IP地址以及端口号都应该是众所周知的。
【函数参数】
sockfd:int类型,表示要进行绑定的socket描述符。addr:struct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边的2.2节会详细说明。addrlen:socklen_t类型,用于指定addr所指向的结构体对应的字节长度。
【返回值】int类型,成功返回0;失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。所以一般来讲,我们在运行服务器端时,若是绑定固定的IP的话,这个IP需要是本地所具有的的IP,否则可能会报以下错误:
1 | Cannot assign requested address |
2.2 sockaddr
这一节就来了解一下这俩结构体之间的关系,以及为什么要使用sockaddr_in,其实还有一个sockaddr_in6,这里就一起说明了。
2.2.1 sockaddr
在使用man查看bind函数的帮助手册的时候,会有这个结构体的说明:
1 | struct sockaddr |
【结构体成员说明】
sa_family表示协议族,它占用2个字节;sa_data是一个char类型的数组,它一共有14个字节,这14个字节中就包括了IP地址、端口号等信息。
【注意事项】这个结构对用户并不友好,它把这些信息都封装在了sa_data数组中,这样使得我们是无法对sa_data数组进行赋值。事实上,这是一个通用的socket地址结构体,一般来讲我们并不会直接使用,而是会选择另一种结构体,然后进行强制类型转换。
2.2.2 sockaddr_in
这个结构体比较奇怪,我看到了三种定义的形式,但是呢,其实他们都是一样的,我们在使用的过程中,正常我们都会看到前两种的定义:
这种定义形式是在网上见的最多的,我上培训班的课程时,老师也是这样来讲的:
1 | struct sockaddr_in |
经过资料查阅,发现使用man 7 ip命令查看帮助手册,也会发现该结构体的定义,定义如下:
1 | struct sockaddr_in |
我从网上搜了一下,这个结构体在netinet/in.h文件中有定义,结构体成员如下:
1 | /* Structure describing an Internet socket address. */ |
但是好像跟我们平时用的不太一样,这个__SOCKADDR_COMMON是什么?它其实是一个宏,它定义在sockaddr.h文件中,我们在终端使用以下命令即可查找该文件所在:
1 | locate sockaddr.h |
当我们打开这个文件,找到__SOCKADDR_COMMON的定义如下:
1 |
它与 ## 连接,表示一个预处理器操作,像上边结构体中成员为:
1 | __SOCKADDR_COMMON (sin_); |
它在预处理完成后将会变成下边的形式:
1 | sa_family_t sin_family |
这样一来,这种形式的结构体定义就与形式一保持一致了,甚至还要比形式一更加的严谨。
上边的三种形式,其实我们按照形式一来理解是比较容易理解的,形式一的结构体中:
第一个成员是
sin_family,占2个字节共8位,这与sockaddr结构体是一致的;第二个成员是
sin_port,占2个字节共8位,表示端口号;第三个成员是
sin_addr,这是一个结构体,struct in_addr类型的结构体成员变量,该结构体内部只有一个成员,就是s_addr,占4个字节共32位,表示IP地址,这里需要注意的是,给该成员赋值的时候,一定要先将IP地址通过函数转换为网络字节序。第四个成员是
sin_zero,这是一个unsigned char类型的数组,这个用于占位的,没有什么实际含义,一般使用memset()函数填充为0。
所以,sockaddr_in结构体实际上是下图这个样子,它是一个保存IPv4地址的结构体。
2.2.3 sockaddr_in6
这个结构体在netinet/in.h文件中有定义:
1 | struct sockaddr_in6 |
这个结构体,与上边的就很类似了,它是一个保存IPv6地址的结构体,成员介绍如下:
sin6_family:sa_family_t类型,协议族,取值为AF_INET6,占2个字节。sin6_port:in_port_t类型,端口号,占2个字节。sin6_flowinfo:uint32_t类型,IPv6流信息,占4个字节。sin6_addr:struct in6_addr类型结构体变量,具体的IPv6地址,占4个字节。sin6_scope_id:uint32_t类型,接口范围ID,占4个字节。
后来发现,该结构体的定义信息我们可以使用man 7 ipv6来打开帮助手册,里边就有这个结构体的详细定义及说明。
2.2.4 sockaddr_un
接下来介绍另一个结构体,在后边是用于UNIX域套接字的绑定信息,在这里也提一下把,在后边学习UNIX域套接字的时候会深入学习,这个结构体的成员为:
1 | struct sockaddr_un |
这个结构体定义在哪里呢?我们可以通过以下命令查看:
1 | man 7 unix |
这样我们便会打开关于unix的帮助手册,里边就有该结构体的说明。
【成员说明】
sun_family:sa_family_t类型,协议族,该字段总是包含AF_UNIX。sun_path:char类型,一个系统文件的绝对路径。
2.2.5 为什么不用sockaddr
经过上边的介绍,我们已经了解了这三个结构体,现在来说一说为什么不用sockaddr结构体。
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如127.0.0.1:80,可是呢,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 和sockaddr_in6来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
2.3 INADDR_ANY
会发现前边的使用了这东西,那这个到底是啥呢?
INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些服务器可能不止一块网卡,多网卡的情况下,这个就表示所有网卡IP地址的意思。
比如如果我们的电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个IP地址了,如果某个应用程序需要监听某个端口,那需要监听哪个网卡地址的端口呢?
如果绑定某个具体的IP地址,那我们只能监听我们所设置的IP地址所在的网卡的端口,我们是无法监听其它两块网卡端口的,如果我们需要三个网卡都监听,那就需要绑定3个IP,也就等于需要管理3个套接字进行数据交换,这样的话就会很麻烦。
为了解决这个问题,于是出现了INADDR_ANY,我们使用bind()只需绑定INADDR_ANY,这样我们就只需要管理一个套接字了,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。这个宏在Linux中的netinet/in.h文件中进行了定义:
1 | /* Address to accept any incoming messages. */ |
这个地址看起来还是比较容易转换的,就是0.0.0.0。在使用的时候最好是进行一下网络字节序的转换。
【注意事项】使用该宏与直接使用"0.0.0.0"作为IP绑定的效果是一样的,只是使用字符串"0.0.0.0"的话需要先转换为二进制,还需要注意网络字节序。
2.4 使用实例
暂无。
3. listen()函数
3.1 函数说明
在linux下可以使用man 2 listen命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数只能在服务器进程中使用,让服务器进程被动进入监听状态,等待客户端的连接请求。它一般在bind()函数之后调用,在accept()函数之前调用。被动监听就是,当没有客户端请求时,套接字处于睡眠状态,只有当接收到客户端请求时,套接字才会被唤醒来响应请求。
【函数参数】
sockfd:int类型,表示已经创建的socket描述符,就是需要进入监听状态的套接字。backlog:int类型,表示用来描述sockfd的等待连接队列能够达到的最大值,一般填5, 测试得知,ARM最大为8。如果将backlog的值设置为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
【返回值】int类型,成功返回0;失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】(1)无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上
执行 listen()。
3.2 请求队列
在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为TCP连接是一个过程,需要经过三次握手,当同时尝试连接的用户过多时,就会使得服务器进程无法快速地完成所有的连接请求。
那这个时候怎么处理上边的问题呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。内核会在自己的进程空间里维护一个队列,这个队列就叫请求队列(Request Queue)。这些连接请求就会被放入请求队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,而这样的一个队列必须有一个大小的上限,这个backlog参数告诉内核使用这个数值作为请求队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。
3.3 使用实例
暂无。
4. accept()函数
4.1 函数说明
在linux下可以使用man 2 accept命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数通常只在服务器端进程中使用,已经进入监听状态的服务器会等待客户端的连接请求,该函数就是用于获取客户端的连接请求并建立连接。
【函数参数】
sockfd:int类型,表示已经创建的socket描述符,就是已经进入监听状态的套接字。addr:struct sockaddr类型的结构体指针变量,这是一个传出参数。指向一个struct sockaddr类型变量,该结构体中会保存成功连接到服务器端的客户端的IP地址及端口号。但是呢,和上边一样,我们一般不使用这个类型,一般会使用struct sockaddr_in类型。但是如果我们对客户端的IP地址与端口号这些信息不感兴趣,可以把addr置为空指针NULL。addrlen:socklen_t类型指针变量,注意与bind()函数的不同,bind()得这个参数传入的是一个值,而这个函数传入的是一个地址,这个参数用于指定addr所指向的结构体对应的字节长度,一般由sizeof求得。如果addr置为空指针NULL,这个参数其实就没有什么太大意义了,就可以也置NULL。
【返回值】int类型,成功返回一个新的socket描述符,这样我们就会拥有两个socket描述符,一个是原来的,可以继续用于监听,新的文件描述符可以用于发送和接收数据,失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.2 函数理解
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
- 调用
socket()函数打开套接字; - 调用
bind()函数将套接字与一个端口号以及IP地址进行绑定; - 调用
listen()函数让服务器进程进入监听状态,监听客户端的连接请求; - 调用
accept()函数处理到来的连接请求。
accept()函数通常只用于服务器应用程序中,如果调用accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字。这个套接字与socket()函数返回的套接字并不同,socket()函数返回的是服务器的套接字(以服务器为例),而accept()函数返回的套接字连接到调用connect()的客户端,服务器通过该套接字与客户端进行数据交互,例如向客户端发送数据、或从客户端接收数据。
所以,理解accept()函数的关键点在于它会创建一个新的套接字,其实这个新的套接字就是与执行connect()(客户端调用connect()向服务器发起连接请求)的客户端之间建立了连接,这个套接字代表了服务器与客户端的一个连接。如果accept()函数执行出错,将会返回-1,并会设置errno以指示错误原因。
4.3 使用实例
暂无。
5. connect()函数
5.1 函数说明
在linux下可以使用man 2 connect命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于客户端应用程序中,客户端调用connect()函数将套接字sockfd与远程服务器进行连接。
【函数参数】
sockfd:int类型,表示已经创建的socket描述符。addr:struct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,指定了待连接的服务器的IP地址以及端口号等信息。但是呢,和上边一样,我们一般不使用这个类型,一般会使用struct sockaddr_in类型。addrlen:socklen_t类型,用于指定addr所指向的结构体对应的字节长度,一般由sizeof求得。
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)客户端通过connect()函数请求与服务器建立连接,对于TCP连接来说,调用该函数将发生TCP连接的握手过程,并最终建立一个TCP连接,而对于UDP协议来说,调用这个函数只是在sockfd中记录服务器IP地址与端口号,而不发送任何数据。
(2)对于TCP协议来说,三次握手建立连接的过程就在调用此函数时完成,当连接成功后,也就代表着三次握手也完成了。
5.2 使用实例
暂无。
6. 断开连接函数
6.1 close()
6.1.1 函数说明
在linux下可以使用man 2 close 命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于关闭一个文件描述符,也就是关闭打开的文件,套接字也是一种文件描述符,所以可以使用该函数来关闭。
【函数参数】
fd:int类型,已打开文件的文件描述符。
【返回值】int类型,成功返回0,失败返回EOF。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)使用该函数会将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,也无法使用与数据收发相关的函数。此时客户端和服务器端之间接收和发送数据都无法进行,使用在TCP协议中的话,会自动触发四次握手断开连接的过程。
(2)调用 close()关闭套接字时会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。默认情况下,close() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,也就意味着,调用 close()将丢失输出缓冲区中的数据。
6.1.2 使用实例
暂无。
6.2 shutdown()
6.2.1 函数说明
在linux下可以使用man 2 shutdown命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于关闭已经建立的连接,而非直接关闭套接字,该函数允许我们只关闭在某个方向上的数据传输。
【函数参数】
sockfd:int类型,表示需要断开连接的socket描述符。how:int类型,表示断开连接的方式。
点击查看 how 的取值及含义
| SHUT_RD | 也就是0,表示断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。 |
| SHUT_WR | 也就是1,表示断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。 |
| SHUT_RDWR | 也就是2,表示同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。 |
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)该函数用于关闭连接,而非关闭套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() 将套接字从内存清除。
(2)调用 shutdown() 关闭输出流时,会向对方发送 FIN 包,表示告诉对方数据传输完毕,不会再有数据了。默认情况下,shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包,这也就意味着,使用 shutdown() 不会丢失缓冲区的数据。
6.2.2 使用实例
暂无。
三、IP地址转换
上边我们已经了解了几个socket函数的使用方法,里边绑定IP的时候用到了一些转换函数,这些是用于转换IP地址以及字节序的,关于字节序的问题,前边其实已经了解过了。对于IP地址,我们定义的时候或者输入的时候一般都是点分十进制表示的字符串形式,但是函数使用的却是整数形式的IP地址,这样我们就需要IP转换为符合函数使用要求的形式。
1. 字节序转换
字节序的转换函数有四个,分别是:
1 | uint32_t htonl(uint32_t hostlong); /* 主机字节序--->网络字节序 */ |
【命名说明】
h为host,表示主机字节顺序;n为net,表示网络字节顺序;l表示无符号整型数据;s表示无符号短整型数据。
1.1 htonl()
1.1.1 函数说明
在linux下可以使用man htonl命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将无符号整型数据hostlong从主机字节顺序转换成网络字节顺序(32位)。
【函数参数】
hostlong:uint32_t类型,表示需要转换的无符号整型数据。
【返回值】uint32_t类型,返回转换后的数据。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | sizeof(uint32_t)=4 |
1.2 htons()
1.2.1 函数说明
在linux下可以使用man htons命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将无符号短整型数据hostshort从主机字节顺序转换为网络字节顺序。
【函数参数】
hostshort:uint16_t类型,表示需要转换的无符号短整型数据。
【返回值】uint16_t类型,返回转换后的数据。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | sizeof(uint16_t)=2 |
1.3 ntohl()
1.3.1 函数说明
在linux下可以使用man ntohl命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将无符号整型数据netlong从网络字节顺序转换为主机字节顺序。
【函数参数】
netlong:uint32_t类型,表示需要转换的无符号整型数据。
【返回值】uint32_t类型,返回转换后的数据。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.3.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | sizeof(uint32_t)=4 |
1.4 ntohs()
1.4.1 函数说明
在linux下可以使用man ntohs命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将无符号短整型数据netshort从网络字节顺序转换为主机字节顺序。
【函数参数】
netshort:uint16_t类型,表示需要转换的无符号短整型数据。
【返回值】uint16_t类型,返回转换后的数据。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.4.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | sizeof(uint16_t)=2 |
2.IP字符串转换
点分十进制字符串和二进制地址之间的转换函数主要有: inet_aton、 inet_addr、inet_ntoa、 inet_ntop、inet_pton 这五个,在我们的应用程序中使用它们需要包含头文件<sys/socket.h>、 <arpa/inet.h>以及<netinet/in.h>。
说明:
inet_aton、 inet_addr、inet_ntoa这些函数可将一个 IP 地址在点分十进制表示形式和二进制表示形式之间进行转换,这些函数已经废弃了, 基本不用这些函数了,但是在一些旧的代码中可能还会看到这些函数。完成此类转换工作我们应该使用这几个: inet_ntop、inet_pton 。
2.1inet_aton()函数
2.1.1 函数说明
在linux下可以使用man inet_aton命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将Internet主机地址cp从IPv4的点号表示法转换成二进制形式(以网络字节顺序),并将其存储在inp指向的结构中。
【函数参数】
cp:char类型指针变量,指向存放IP地址的内存空间。inp:struct in_addr类型,存放转换后的IP地址。
点击查看 struct in_addr 成员
该结构体定义在netinet/in.h中:
1 | typedef uint32_t in_addr_t; |
【返回值】int类型,成功返回1,失败或者字符串无效返回0。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | ret = 1 |
2.2inet_addr()函数
2.2.1 函数说明
在linux下可以使用man inet_addr命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于将Internet主机地址cp从IPv4的点号表示法转换成网络字节顺序的二进制数据。
【函数参数】
cp:char类型指针变量,指向存放IP地址的内存空间。
【返回值】in_addr_t类型,成功返回转换后的IP地址,失败或者字符串无效返回INADDR_NONE ,这一般是-1,需要注意的是这是有问题的,因为-1在in_addr_t类型下在计算机中为0xffff ffff(补码),这其实是一个有效的地址(255.255.255.255)。
点击查看 in_addr_t 类型
该类型定义在netinet/in.h中:
1 | typedef uint32_t in_addr_t; |
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)内部包含了字节序的转换,默认是网络字节序的模式。
(2)仅适用于IPV4。
(3)当出错时,返回-1,此函数不能用于255.255.255.255的转换。
2.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | cp1 = 192.168.0.1, ret1 = 0x100a8c0 |
2.3inet_ntoa()函数
2.3.1 函数说明
在linux下可以使用man inet_ntoa命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将以网络字节顺序给出的Internet主机二进制地址转换为IPv4点分十进制格式的字符串。该字符串在静态分配的缓冲区中返回,后续调用将覆盖该缓冲区。
【函数参数】
in:struct in_addr类型,内部只有一个成员s_addr,表示以网络字节顺序给出的Internet主机二进制地址。
点击查看 struct in_addr 成员
该结构体定义在netinet/in.h中:
1 | typedef uint32_t in_addr_t; |
【返回值】char类型指针变量,成功返回转换后的点分十进制IPv4地址,否则返回NULL。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.3.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | cp = 192.168.0.1, addr.s_addr = 0x100a8c0 |
2.4inet_pton()函数
2.4.1 函数说明
在linux下可以使用man inet_pton命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将点分十进制表示的字符串形式IP转换成网络字节序的二进制IPv4或IPv6地址。
【函数参数】
af:int类型,协议族,这里必须是AF_INET或AF_INET6,其中AF_INET表示目标地址为IPv4地址,AF_INET6表示目标地址为IPv6地址。src:char类型指针变量,指向需要转换的字符串形式的IP地址。dst:void类型指针变量,转换后得到的地址存放在参数dst所指向的对象中。
点击查看参数类型说明
当参数af被指定为AF_INET,则参数dst所指对象应该是一个struct in_addr结构体的对象;如果参数af被指定为AF_INET6,则参数dst所指对象应该是一个struct in6_addr结构体的对象。这两个结构体都定义在netinet/in.h文件中:
1 | /* Internet address. */ |
【返回值】int类型,成功返回1(已成功转换)。如果src不包含表示指定地址族中有效网络地址的字符串,则返回0;如果af不包含有效的地址族,则返回-1并将errno设置为EAFNOSUPPORT。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)内部包含了字节序的转换,默认是网络字节序的模式。
(2)适用于IPV4和IPv6。
(3)此函数可以用于255.255.255.255的转换。
(4)注意src为一个字符串,若是想要填充INADDR_ANY以达到保定所有IP的目的,最好还是使用其他的函数比较好,这是因为INADDR_ANY其实是一个in_addr_t类型,它直接就是一个二进制形式的IPv4地址。
2.4.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | src1 = 192.168.0.1, addr1.s_addr = 0x100a8c0 |
2.5inet_ntop()函数
2.5.1 函数说明
在linux下可以使用man inet_ntop命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数将网络字节序的二进制IPv4或IPv6地址变成本地的字符串形式的IP地址。
【函数参数】
af:int类型,协议族,这里必须是AF_INET或AF_INET6,其中AF_INET表示目标地址为IPv4地址,AF_INET6表示目标地址为IPv6地址。src:void类型指针变量,需要转换的网络字节序的二进制IPv4或IPv6地址存放在参数src所指向的对象中。
点击查看参数类型说明
当参数af被指定为AF_INET,则参数src所指对象应该是一个struct in_addr结构体的对象;如果参数af被指定为AF_INET6,则参数src所指对象应该是一个struct in6_addr结构体的对象。这两个结构体都定义在netinet/in.h文件中:
1 | /* Internet address. */ |
dst:char类型指针变量,转换完成的字符串存放在参数dts所指的缓冲区中。size:socklen_t类型,指定了dst所指向缓冲区的大小,也就是存放转换完成的字符串的内存大小。
【返回值】char类型指针变量,成功返回转换完成后所得到字符串的首地址。失败返回NULL并将errno设置表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)适用于IPV4和IPv6。
(2)此函数可以用于255.255.255.255的转换。
2.5.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | src1 = 192.168.0.1, addr1.s_addr = 0x100a8c0 |
四、数据传输
上边我们已经了解了建立一个网络通信的连接所需的相关函数,一旦客户端和服务器端建立好连接后,就可以通过套接字进行数据的传输了。对于客户端使用socket()返回的套接字描述符,而对于服务器来说,需要使用accept()返回的套接字描述符,注意这里服务器使用的并不是程序一开始的时候创建的那个socket描述符,原来的那个会继续用于监听。
1. read()/write()
read()和write()这两个函数一般用于面向连接的socket上进行数据传输。也就是说这两个函数一般用于TCP协议编程中。
1.1 read()
1.1.1 函数说明
在linux下可以使用man 2 read命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以从文件中读取数据。
【函数参数】
fd:int类型,表示已打开的文件描述符。buf:void类型指针变量,表示接收数据的缓冲区。count:size_t类型,表示需要读取的字节数,不应超过buf。
【返回值】ssize_t类型(表示有符号的size_t),成功时返回实际读取的字节数;出错时返回EOF,读到文件末尾时返回0。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】该函数可以在很多场景下用于读取数据,在网络编程中,套接字创建好直接会有一个打开的文件描述符,所以就不需要再使用其他的相关函数打开一个文件描述符了。
1.1.2 使用实例
暂无
1.2 write()
1.2.1 函数说明
在linux下可以使用man 2 write命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于向文件写入数据。
【函数参数】
fd:int类型,表示已打开的文件描述符。buf:void类型指针变量,表示要写入文件的数据的缓冲区。count:size_t类型,表示需要写入的字节数,不应超过buf。
【返回值】ssize_t类型,成功时返回实际写入的字节数;出错时返回EOF。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】该函数可以在很多场景下用于写入数据,在网络编程中,套接字创建好直接会有一个打开的文件描述符,所以就不需要再使用其他的相关函数打开一个文件描述符了。
1.2.2 使用实例
暂无
2.recv()/send()
recv()和send()这两个函数一般用于面向连接的socket上进行数据传输。也就是说这两个函数一般用于TCP协议编程中。
2.1 recv()
2.1.1 函数说明
在linux下可以使用man 2 recv命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以用于接收来自socket套接字的数据。
【函数参数】
sockfd:int类型,表示已打开的socket描述符。buf:void类型指针变量,表示接收数据的缓冲区。len:size_t类型,表示需要读取的字节数,不应超过buf。flags:int类型,指定一些标志用于控制如何接收数据。一般来说,这里我们选择0,这样这个函数就跟read()一样了。
点击查看flags常见可取值及含义
| 标志 | 描述 |
| MSG_CMSG_CLOEXEC | 为UNIX域套接字上接收的文件描述符设置执行时关闭标志 |
| MSG_DONTWAIT | 启动非阻塞操作(相当于O_NONBLOCK) |
| MSG_ERRQUEUE | 接收错误信息作为辅助数据 |
| MSG_OOB | 如果协议支持,获取带外数据 |
| MSG_PEEK | 返回数据包内容而不真正取走数据包 |
| MSG_TRUNC | 即使数据包被截断,也返回数据包的长度 |
| MSG_WAITALL | 等待直到所有的数据可用(仅SOCK_STREAM) |
【返回值】ssize_t类型(表示有符号的size_t),成功时返回实际读取的字节数;出错时返回-1,并设置errno表示错误类型。如果发送者已经调用shutdown来结束传输,或者网络协议支持按默认的顺序关闭并且发送端已经关闭,那么当所有的数据接收完毕后,recv()会返回0(传统的“文件结束”返回)。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
暂无
2.2 send()
2.2.1 函数说明
在linux下可以使用man 2 send命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以用于向socket套接字发送数据。
【函数参数】
sockfd:int类型,表示已打开的socket描述符。buf:void类型指针变量,表示要发送数据的缓冲区。len:size_t类型,表示需要发送的数据的字节数,不应超过buf。flags:int类型,指定一些标志用于控制如何发送数据,这也是该函数与write()函数的唯一区别。一般来说,这里我们选择0,这样这个函数就跟write()一样了。
点击查看flags常见可取值及含义
| 标志 | 描述 |
| MSG_CONFIRM | 提供链路层反馈以保持地址映射有效 |
| MSG_DONTROUTE | 勿将数据包路由出本地网络 |
| MSG_DONTWAIT | 允许非阻塞操作(等价于使用 O_NONBLOCK) |
| MSG_EOR | 如果协议支持,标志记录结束 |
| MSG_MORE | 延迟发送数据包允许写更多数据 |
| MSG_NOSIGNAL | 在写无连接的套接字时不产生 SIGPIPE 信号 |
| MSG_OOB | 如果协议支持,发送带外数据 |
【返回值】ssize_t类型(表示有符号的size_t),成功时返回发送的字节数;出错时返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当send()成功返回时,数据已经被无错误的发送到网络驱动程序上。
2.2.2 使用实例
暂无
3.recvfrom()/sendto()
recvfrom()和sendto()一般用于在面向无连接的数据报socket方式下进行数据传输。也就是说,这两个函数一般用于UDP协议编程中。
3.1 recvfrom()
3.1.1 函数说明
在linux下可以使用man 2 recvfrom命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以用于接收socket套接字的数据,并获取客户端的IP和端口号信息。
【函数参数】
sockfd:int类型,表示已打开的socket描述符。buf:void类型指针变量,表示接收数据的缓冲区。len:size_t类型,表示需要读取的字节数,不应超过buf。flags:int类型,指定一些标志用于控制如何接收数据。一般来说,我们这里会选择填0。
点击查看flags常见可取值及含义
| 标志 | 描述 |
| MSG_CMSG_CLOEXEC | 为UNIX域套接字上接收的文件描述符设置执行时关闭标志 |
| MSG_DONTWAIT | 启动非阻塞操作(相当于O_NONBLOCK) |
| MSG_ERRQUEUE | 接收错误信息作为辅助数据 |
| MSG_OOB | 如果协议支持,获取带外数据 |
| MSG_PEEK | 返回数据包内容而不真正取走数据包 |
| MSG_TRUNC | 即使数据包被截断,也返回数据包的长度 |
| MSG_WAITALL | 等待直到所有的数据可用(仅SOCK_STREAM) |
src_addr:struct sockaddr类型结构体指针变量,这是一个传出参数,里边保存了接收的数据的来源信息,包括了客户端的IP和端口号。这个参数其实与bind()函数的第二个参数的用法类似,不过这里是一个结构体指针变量。我们一般都会选择使用sockaddr_in或者sockaddr_in6来获取数据发送方的信息。但是如果我们对客户端的IP地址与端口号这些信息不感兴趣,可以把addr置为空指针NULL。addrlen:socklen_t类型指针变量,这个参数用于指定src_addr所指向的结构体对应的字节长度,一般由sizeof求得。如果src_addr置为空指针NULL,这个参数其实就没有什么太大意义了,就可以也置NULL。
【返回值】ssize_t类型(表示有符号的size_t),成功时返回实际读取的字节数;出错时返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】
(1)当该函数最后两个参数为NULL的时候,它可以等价于recv(),即:
1 | recv(sockfd, buf, len, flags); |
(2)该函数会阻塞等待客户端数据的到来。
3.1.2 使用实例
暂无
3.2 sendto()
3.2.1 函数说明
在linux下可以使用man 2 sendto命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数可以用于向socket套接字发送数据。
【函数参数】
sockfd:int类型,表示已打开的socket描述符。buf:void类型指针变量,表示要发送数据的缓冲区。len:size_t类型,表示需要发送的数据的字节数,不应超过buf。flags:int类型,指定一些标志用于控制如何发送数据。一般来说,这里我们选择0。
点击查看flags常见可取值及含义
| 标志 | 描述 |
| MSG_CONFIRM | 提供链路层反馈以保持地址映射有效 |
| MSG_DONTROUTE | 勿将数据包路由出本地网络 |
| MSG_DONTWAIT | 允许非阻塞操作(等价于使用 O_NONBLOCK) |
| MSG_EOR | 如果协议支持,标志记录结束 |
| MSG_MORE | 延迟发送数据包允许写更多数据 |
| MSG_NOSIGNAL | 在写无连接的套接字时不产生 SIGPIPE 信号 |
| MSG_OOB | 如果协议支持,发送带外数据 |
dest_addr:struct sockaddr类型结构体指针变量,里边保存对方(接收数据一方)的IP和端口号。这个参数其实与bind()函数的第二个参数的用法类似,不过这里是一个结构体指针变量。我们一般都会选择使用sockaddr_in或者sockaddr_in6来获取数据发送方的信息。但是如果我们对客户端的IP地址与端口号这些信息不感兴趣,可以把addr置为空指针NULL。addrlen:socklen_t类型,这个参数用于指定dest_addr所指向的结构体对应的字节长度,一般由sizeof求得。如果dest_addr置为空指针NULL,这个参数其实就没有什么太大意义了,就可以为0了。
【返回值】ssize_t类型(表示有符号的size_t),成功时返回发送的字节数;出错时返回-1,并设置errno表示错误类型。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】当该函数最后两个参数为NULL的时候,它可以等价于recv(),即:
1 | send(sockfd, buf, len, flags); |
3.2.2 使用实例
暂无
五、socket缓冲区和阻塞模式
1. socket缓冲区
1.1 缓冲区说明
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,因为这些都是TCP协议负责的事情。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由我们控制。
TCP协议套接字的I/O缓冲区如下所示:
I/O缓冲区的特性总结如下:
I/O缓冲区在每个TCP套接字中单独存在;I/O缓冲区在创建套接字时自动生成;- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
1.2 TCP协议缓冲区大小获取
- 方法一
输入输出缓冲区的默认大小可以通过 getsockopt() 函数(这个函数会在后边的网络属性设置中详细学习)获取:
1 |
|
然后在终端输入以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | sendBuff length: 16KB |
- 方法二
我们在终端输入以下命令:
1 | cat /proc/sys/net/ipv4/tcp_wmem # 获取 TCP 发送缓冲区大小 |
上边是两条命令,我们将会看到如下的信息输出:
我们可以看到每条命令都是打印了三个数据,这三个数据分别为:最小值、默认值和最大值(我的系统是Ubuntu21.04的64位版本)。
1.3 BUFSIZ
在有些网络编程的程序中看到过这个宏,找半天没找到定义,后来才发现这是C库中定义的一个宏,在网上搜,都说它定义在stdio.h中,我们可以使用以下命令查看该文件的位置:
1 | locate stdio.h |
然后我们选择打开路径为/usr/include/stdio.h的这个文件:
1 | vim /usr/include/stdio.h |
就会发现有如下宏:
1 | /* Default buffer size. */ |
这个宏的大小是8192,所以我们可以通过它来作为数组的大小,可以用于网络编程的数据输入输出缓冲区数组的定义。
2. 阻塞模式
对于TCP套接字(默认情况下),TCP套接字的阻塞模式如下:
- 当使用
write()/send()发送数据时
(1)首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,有足够的空间时,才唤醒 write()/send() 函数继续写入数据。
(2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
(3)如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
(4)直到所有数据被写入缓冲区 write()/send() 才能返回。
- 当使用
read()/recv()读取数据时
(1)首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
(2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
(3)直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
TCP套接字默认情况下是阻塞模式,也是最常用的。当然我们也可以更改为非阻塞模式。