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套接字默认情况下是阻塞模式,也是最常用的。当然我们也可以更改为非阻塞模式。