网络通信概述
网络通信本质上是一种进程间通信,是位于网络中不同主机上的进程之间的通信,属于 IPC
的一种,通常称为 socket IPC
,在第十章中给大家简单地提到过,如图
10.2.1
中所示。所以网络通信是为了解决在网络环境中,不同主机上的应用程序之间的通信问题。
大概可以分为三个层次,如下所示:
(
1
)、硬件层:网卡设备,收发网络数据
(2)、驱动层:网卡驱动(
Linux
内核网卡驱动代码)
(3)、应用层:上层应用程序(调用
socket
接口或更高级别接口实现网络相关应用程序)
硬件层
在硬件上,两台主机都提供了网卡设备,也就满足了进行网络通信最基本的要求,网卡设备是实现网络数据收发的硬件基础。并且通信的两台主机之间需要建立网络连接,这样两台主机之间才可以进行数据传输,譬如通过网线进行数据传输。网络数据的传输媒介有很多种,大体上分为有线传输(譬如双绞线网线、光纤等)和无线传输(譬如 WIFI、蓝牙、
ZigBee
、
4G/5G/GPRS
等),
PC
机通常使用有线网络,而手机等移动设备通常使用无线网络。
内核层
在内核层,提供了网卡驱动程序,可以驱动底层网卡硬件设备,同时向应用层提供 socket 接口。
应用层
在应用层,应用程序基于内核提供的 socket
接口进行应用编程,实现自己的网络应用程序。需要注意的是,socket
接口是内核向应用层提供的一套网络编程接口,所以我们学习网络编程其实就是学习
socket
编程,如何基于 socket
接口编写应用程序。
除了 socket
接口之外,在应用层通常还会使用一些更为高级的编程接口,譬如
http
、网络控件等,那么这些接口实际上是对 socket
接口的一种更高级别的封装。
网络互连模型:OSI 七层模型
七层模型,亦称 OSI
(
Open System Interconnection
)。
OSI
七层参考模型是国际标准化组织(
ISO
)制定的一个用于计算机或通信系统间网络互联的标准体系,一般称为 OSI
参考模型或七层模型。
OSI
七层模型是一个网络互连模型,
TCP/IP 四层/五层模型(实际用的五层)
应用层,表示层,会话层合为应用层
应用层
应用层(Application Layer
)是
OSI
参考模型中的最高层,是最靠近用户的一层,为上层用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP
、
FTP
、
TFTP
、
SMTP
、 SNMP、
DNS
、
TELNET
、
HTTPS
、
POP3
、
DHCP
。
传输层
传输层(Transport Layer
)定义传输数据的协议端口号,以及端到端的流控和差错校验。该层建立了主机端到端的连接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括差错校验处理和流控等问题。我们通常说的,TCP
、UDP
协议就工作在这一层,端口号既是这里的“端”。
网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。本层通过 IP
寻址来建立两个节点之间的连接,为源端发送的数据包选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。网络层 (Network Layer
)也就是通常说的
IP
层。该层包含的协议有:
IP
(
Ipv4
、
Ipv6
)、
ICMP
、
IGMP
等。
数据链路层
数据链路层(Data Link Layer
)是
OSI
参考模型中的第二层,负责建立和管理节点间逻辑连接、进行硬件地址寻址、差错检测等功能。将比特组合成字节进而组合成帧,用 MAC
地址访问介质,错误发现但不能纠正。
数据链路层又分为 2 个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。MAC 子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的访问控制;LLC 子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。
数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。
物理层
物理层(Physical Layer
)是
OSI 参考模型的最低层,物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
实际上,网络数据信号的传输是通过物理层实现的,通过物理介质传输比特流。物理层规定了物理设备标准、电平、传输速率等。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆等,这些都是物理层的传输介质。
数据的封装与拆封
网络通信中,数据从上层到下层交付时,要进行封装;同理,当目标主机接收到数据时,数据由下层传递给上层时需要进行拆封。这就是数据的封装与拆封。
当用户发送数据时,将数据向下交给传输层,但是在交给传输层之前,应用层相关协议会对用户数据进行封装,譬如 MQTT
、
HTTP
等协议,其实就是在用户数据前添加一个应用程序头部,这是处于应用层的操作,最后应用层通过调用传输层接口来将封装好的数据交给传输层。
传输层会在数据前面加上传输层首部(此处以 TCP
协议为例,图中的传输层首部为
TCP
首部,也可以是 UDP
首部),然后向下交给网络层。
同样地,网络层会在数据前面加上网络层首部(IP 首部),然后将数据向下交给链路层,链路层会对数据进行最后一次封装,即在数据前面加上链路层首部(此处使用以太网接口为例,对应以太网首部),然后将数据交给网卡。
最后,由网卡硬件设备将数据转换成物理链路上的电平信号,数据就这样被发送到了网络中。这就是网络数据的发送过程,从图中可以看到,各层协议均会对数据进行相应的封装,可以概括为 TCP/IP
模型中的各层协议对数据进行封装的过程。
IP 地址
IP 地址是软件地址,不是硬件地址,硬件 MAC 地址是存储在网卡中的,应用于局域网中寻找目标主机
IP 地址的编址方式
互联网中的每一台主机都需要一个唯一的 IP
地址以标识自己的身份,那么
IP
地址究竟是什么,如何去定义一个 IP
呢?我们需要对
IP
地址的编址方式进行了解。
传统的 IP
地址是一个
32
位二进制数的地址,也叫
IPv4
地址,由
4
个
8
位字段组成。除了
IPv4
之外, 还有 IPv6
,
IPv6
采用
128
位地址长度,
8
个
16
位字段组成,本小节我们暂时不去理会
IPv6
地址。
在网络通信数据包中,IP
地址以
32
位二进制的形式表示;而在人机交互中,通常使用点分十进制方式表示,譬如 192.168.1.1
,这就是点分十进制的表示方式。
IP 地址中的
32
位实际上包含
2
部分,分别为网络地址和主机地址,可通过子网掩码来确定网络地址和主机地址分别占用多少位。
如何判断
2
个
IP
地址是否在同一个网段内
如何判断两个 IP
地址是否处于同一个子网,可通过网络标识来进行判断,网络标识定义如下:
网络标识 = IP
地址
&
子网掩码
2 个
IP
地址的网络标识相同,那么它们就处于同一网络。譬如
192.168.1.50
和
192.168.1.100
,这
2
个都是 C
类地址,对应的子网掩码为
255.255.255.0
,很明显,这两个
IP
地址与子网掩码进行按位与操作时得到的结果(网络标识)是一样的,所以它们处于同一网络。
TCP 协议
关于
TCP
协议我们需要理解的重点如下:
①、
TCP
协议工作在传输层,对上服务
socket
接口,对下调用
IP
层;
②、
TCP
是一种面向连接的传输协议,通信之前必须通过三次握手与客户端建立连接关系后才可通信;
③、
TCP
协议提供可靠传输,不怕丢包、乱序。
建立
TCP
连接:三次握手
前面我们提到过,TCP
协议是一个面向连接的协议,双方在进行网络通信之间,都必须先在双方之间 建立一条连接,俗称“握手”,可能在学习网络编程之前,大家或多或少都听过“三次握手”、“四次挥手” 这些词语,那么“三次握手”、“四次挥手”究竟是什么意思,本小节将详细讨论一个 TCP
连接是如何建 立的,需要经过哪些过程。
“三次握手”其实是指建立 TCP
连接的一个过程,通信双方建立一个
TCP
连接需要经过“三次握手” 这样一个过程。
首先建立连接的过程是由客户端发起,而服务器会时刻监听、等待着客户端的连接,其示意图如下所示:
关闭 TCP 连接:四次挥手
除了“三次握手”,还有“四次挥手”,“四次挥手”(有一些书也会称为四次握手)其实是指关闭 TCP 连接的一个过程,当通信双方需要关闭 TCP
连接时需要经过“四次挥手”这样一个过程。
四次挥手即终止 TCP
连接,就是指断开一个
TCP
连接时,需要客户端和服务端总共发送
4
个包以确认连接的断开。在 socket
编程中,这一过程由客户端或服务端任一方执行
close
来触发。
由于 TCP
连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个 FIN
来终止这一方向的连接,收到一个
FIN
只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个 TCP
连接上仍然能够发送数据,直到这一方向也发送了
FIN
。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
TCP 状态说明
TCP 协议在建立连接、断开连接以及数据传输过程中都会呈现出现不同的状态,不同的状态采取的动作也是不同的,需要处理各个状态之间的关系。图 29.5.2
、图
29.5.3
以及图
29.5.4
中就出现了一些状态标志,除了这些状态标志之外,还有其它一些 TCP
状态,对这些
TCP
状态的说明如下所示:
⚫
CLOSED
状态:
表示一个初始状态。
⚫
LISTENING
状态:
这是一个非常容易理解的状态,表示服务器端的某个
SOCKET
处于监听状态, 监听客户端的连接请求,可以接受连接了。譬如服务器能够提供某种服务,它会监听客户端 TCP 端口的连接请求,处于 LISTENING
状态,端口是开放的,等待被客户端连接。
⚫
SYN_SENT
状态
(
客户端状态
)
:
当客户端调用
connect()
函数连接时,它首先会发送
SYN
报文给服务器请求建立连接,因此也随即它会进入到了 SYN_SENT
状态,并等待服务器的发送三次握手中的第 2
个报文。
SYN_SENT
状态表示客户端已发送
SYN
报文。
⚫
SYN_REVD
状态
(
服务端状态
)
:
这个状态表示服务器接受到了
SYN
报文,在正常情况下,这个状态是服务器端的 SOCKET
在建立
TCP
连接时的三次握手过程中的一个中间状态,很短暂,基本上用 netstat
你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次
TCP
握手过程中最后一个 ACK
报文不予发送。因此这种状态时,当收到客户端的
ACK
报文后,它会进入到 ESTABLISHED
状态。
⚫
ESTABLISHED
状态:
这个容易理解了,表示连接已经建立了。
⚫
FIN_WAIT_1
和
FIN_WAIT_2
状态:
其实
FIN_WAIT_1
和
FIN_WAIT_2
状态的真正含义都是表
示等待对方的
FIN
报文。而这两种状态的区别是:
FIN_WAIT_1
状态实际上是当
SOCKET
在
ESTABLISHED
状态时,它想主动关闭连接,向对方发送了
FIN
报文,此时该
SOCKET
即进入到
FIN_WAIT_1
状态。而当对方回应
ACK
报文后,则进入到
FIN_WAIT_2
状态,当然在实际的正常
情况下,无论对方何种情况下,都应该马上回应
ACK
报文,所以
FIN_WAIT_1
状态一般是比较难
见到的,而
FIN_WAIT_2
状态还有时常常可以用
netstat
看到。
⚫
TIME_WAIT
状态:
表示收到了对方的
FIN
报文,并发送出了
ACK
报文,就等
2MSL
后即可回
到
CLOSED
可用状态了。如果
FIN_WAIT_1
状态下,收到了对方同时带
FIN
标志和
ACK
标志的
报文时,可以直接进入到
TIME_WAIT
状态,而无须经过
FIN_WAIT_2
状态。
⚫
CLOSE_WAIT
状态:
这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方
close
一个
SOCKET
后发送
FIN
报文给自己,你系统毫无疑问地会回应一个
ACK
报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方, 如果没有的话,那么你也就可以 close
这个
SOCKET
,发送
FIN
报文给对方,也即关闭连接。所以你在 CLOSE_WAIT
状态下,需要完成的事情是等待你去关闭连接。
⚫
LAST_ACK
状态:
它是被动关闭一方在发送
FIN
报文后,最后等待对方的
ACK
报文。当收到
ACK 报文后,也即可以进入到 CLOSED
状态了。
UDP
协议
除了 TCP
协议外,还有
UDP
协议,想必大家都听过说,
UDP
是
User Datagram Protocol
的简称,中文名是用户数据报协议,是一种无连接、不可靠的协议,同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过 IP
层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外,UDP 协议更没有流量控制、拥塞控制等功能,在发送的一端,UDP 只是把上层应用的数据封装到 UDP 报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到 IP 数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP 报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。
UDP 协议的特点:
①、无连接、不可靠;
②、尽可能提供交付数据服务,出现差错直接丢弃,无反馈;
③、面向报文,发送方的
UDP
拿到上层数据直接添加个
UDP
首部,然后进行校验后就递交给
IP
层, 而接收的一方在接收到 UDP
报文后简单进行校验,然后直接去除数据递交给上层应用;
④、速度快,因为
UDP
协议没有
TCP
协议的握手、确认、窗口、重传、拥塞控制等机制,
UDP
是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候 UDP
也不会降低发送的数据。
UDP 虽然有很多缺点,但也有自己的优点,所以它也有很多的应用场合,因为在如今的网络环境下, UDP 协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以,UDP 协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。
端口号的概念
前面给大家介绍了 IP
地址,互联网中的每一台主机都需要一个唯一的
IP
地址以标识自己的身份,网络中传输的数据包通过 IP
地址找到对应的目标主机;一台主机通常只有一个
IP
地址,但主机上运行的网络进程却通常不止一个,譬如 Windows 电脑上运行着
QQ
、微信、钉钉、网页浏览器等,这些进程都需要进行网络连接,它们都可通过网络发送/
接收数据,那么这里就有一个问题?主机接收到网络数据之后,如何确定该数据是哪个进程对应的接收数据呢?其实就是通常端口号来确定的。
端口号本质上就是一个数字编号,用来在一台主机中唯一标识一个能上网(能够进行网络通信)的进程,端口号的取值范围为 0~65535
。一台主机通常只有一个
IP
地址,但是可能有多个端口号,每个端口号表示一个能上网的进程。一台拥有 IP
地址的主机可以提供许多服务,比如
Web
服务、
FTP
服务、
SMTP
服务等,这些服务都是能够进行网络通信的进程,IP
地址只能区分网络中不同的主机,并不能区分主机中的这些进程,显然不能只靠 IP
地址,因此才有了端口号。通过“
IP
地址
+
端口号”来区分主机不同的进程。
很多常见的服务器它都有特定的端口号,具体详情如下表所示
==================================================================================================================================================
socket 编程基础
socket 是内核向应用层提供的一套网络编程接口,用户基于 socket
接口可开发自己的网络相关应用程序。
socket 简介
套接字(socket
)是
Linux
下的一种进程间通信机制(
socket IPC
),在前面的内容中已经给大家提到过, 使用 socket IPC
可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC
通常使用客户端
<--->
服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了 socket
接口,对于应用程序开发人员来说,我们只需要调用 socket
接口开发自己的应用程序即可!socket
是应用层与
TCP/IP
协议通信的中间软件抽象层,它是一组接口。在设计模式中,socket 其实就是一个门面模式,它把复杂的
TCP/IP
协议隐藏在
socket
接口后面,对用户来说,一组简单的接口就是全部,让 socket
去组织数据,以符合指定的协议。所以,我们无需深入的去理解
tcp/udp
等各种复杂的 TCP/IP
协议,
socket
已经为我们封装好了,我们只需要遵循
socket
的规定去编程,写出的程序自然遵循 tcp/udp
标准的。
当前网络中的主流程序设计都是使用 socket
进行编程的,因为它简单易用,它还是一个标准(
BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于
socket
接口编写的,那么它可以移植到 任何实现 BSD socket
标准的平台,譬如
LwIP
,它兼容
BSD Socket
;又譬如
Windows
,它也实现了一套基于 socket 的套接字接口,更甚至在国产操作系统中,如
RT-Thread
,它也实现了
BSD socket
标准的
socket
接 口。
socket 编程接口介绍
本小节我们向大家介绍,socket
编程中使用到的一些接口函数。使用
socket
接口需要在我们的应用程序代码中包含两个头文件:
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>
socket()
函数
socket()函数类似于
open()
函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为 socket
描述符(
socket descriptor
),这个
socket
描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
#include <sys/types.h> /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);
domain参数 domain 用于指定一个通信域;这将选择将用于通信的协议族type参数 type 指定套接字的类型protocol参数 protocol 通常设置为 0 ,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议( Transmission Control Protocol , TCP 协议)。在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP 。
bind()函数
bind()函数用于将一个
IP
地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址---
即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址 (注意这里说的地址包括 IP
地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的 IP
地址以及对应的端口号,所以通常服务器的
IP
地址以及端口号都是众所周知的。
调用 bind()
函数将参数
sockfd
指定的套接字与一个地址
addr
进行绑定,成功返回
0
,失败情况下返回
- 1,并设置
errno
以提示错误原因。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen()
函数
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,
listen()
函数在一般在 bind()
函数之后调用,在
accept()
函数之前调用,它的函数原型是:
无法在一个已经连接的套接字(即已经成功执行 connect()
的套接字或由
accept()
调用返回的套接字)上执行 listen()
。
参数 backlog
用来描述
sockfd
的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP
连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个 backlog
参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
accept()
函数
服务器调用 listen()
函数之后,就会进入到监听状态,等待客户端的连接请求,使用
accept()
函数获取客户端的连接请求并建立连接。函数原型如下所示:
为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
①、调用
socket()
函数打开套接字;
②、调用
bind()
函数将套接字与一个端口号以及
IP
地址进行绑定;
③、调用
listen()
函数让服务器进程进入监听状态,监听客户端的连接请求;
④、调用
accept()
函数处理到来的连接请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
connect()
函数
该函数用于客户端应用程序中,客户端调用 connect()
函数将套接字 sockfd
与远程服务器进行连接,参数 addr
指定了待连接的服务器的
IP
地址以及端口号等信息,参数
addrlen
指定了
addr
指向的
struct sockaddr对象的字节大小。 客户端通过 connect()
函数请求与服务器建立连接,对于
TCP
连接来说,调用该函数将发生
TCP
连接的握手过程,并最终建立一个 TCP
连接,而对于
UDP
协议来说,调用这个函数只是在
sockfd
中记录服务器IP 地址与端口号,而不发送任何数据。
函数调用成功则返回
0
,失败返回
-1
,并设置
errno
以指示错误原因
发送和接收函数
1)
read()
函数
read()函数大家都很熟悉了,通过
read()
函数从一个文件描述符中读取指定字节大小的数据并放入到指定的缓冲区中,read()
调用成功将返回读取到的字节数,此返回值受文件剩余字节数限制,当返回值小于指定的字节数时并不意味着错误;这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数据,或者 read()
函数被信号中断等),出错返回
-1
并设置
errno
,如果在调 read
之前已到达文件末尾,则这次
read
返回
0
。
套接字描述符也是文件描述符,所以使用 read()
函数读取网络数据时,
read()
函数的参数
fd
就是对应的套接字描述符。
2)recv()
函数
不论是客户端还是服务器都可以通过 revc()
函数读取网络数据,它与
read()
函数的功能是相似的。参数 sockfd 指定套接字描述符,参数
buf
指向了一个数据接收缓冲区,参数
len
指定了读取数据的字节大小,参数 flags
可以指定一些标志用于控制如何接收数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
3)write()
函数
通过 write()
函数可以向套接字描述符中写入数据,函数调用成功返回写入的字节数,失败返回
-1
,并设置 errno
变量。
4)send()
函数
send 和 write 很相似,但是 send 可以通过参数 flags 指定一些标志,来改变处理传输数据的方式。这些
标志如下所示:
即使 send()
成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当
send 成功返回时,数据已经被无错误的发送到网络驱动程序上。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
5)close()
关闭套接字
当不再需要套接字描述符时,可调用 close()
函数来关闭套接字,释放相应的资源。
IP 地址格式转换函数
对于人来说,我们更容易阅读的是点分十进制的 IP
地址,譬如
192.168.1.110
、
192.168.1.50
,这其实是 一种字符串的形式,但是计算机所需要理解的是二进制形式的 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
函数
inet_ntop()、
inet_pton()
与
inet_ntoa()
、
inet_aton()
类似,但它们还支持
IPv6
地址。它们将二进制
Ipv4
或 Ipv6 地址转换成以点分十进制表示的字符串形式,或将点分十进制表示的字符串形式转换成二进制
Ipv4
或 Ipv6 地址。使用这两个函数只需包含
<arpa/inet.h>
头文件即可!
inet_pton()
函数
inet_pton()函数将点分十进制表示的字符串形式转换成二进制
Ipv4
或
Ipv6
地址。 将字符串 src
转换为二进制地址,参数
af
必须是
AF_INET
或
AF_INET6
,
AF_INET
表示待转换的
Ipv4
地址,
AF_INET6
表示待转换的是
Ipv6
地址;并将转换后得到的地址存放在参数
dst
所指向的对象中,如果参数 af
被指定为
AF_INET
,则参数
dst
所指对象应该是一个
struct in_addr
结构体的对象;如果参数
af
被指定为 AF_INET6
,则参数
dst
所指对象应该是一个
struct in6_addr
结构体的对象。
inet_pton()转换成功返回 1
(已成功转换)。如果
src
不包含表示指定地址族中有效网络地址的字符串, 则返回 0
。如果
af
不包含有效的地址族,则返回
-1
并将
errno
设置为
EAFNOSUPPORT
。
int inet_pton(int af, const char *src, void *dst);
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define IPV4_ADDR "192.168.1.222"
int main(void)
{
struct in_addr addr;
inet_pton(AF_INET, IPV4_ADDR, &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
exit(0);
}
inet_ntop()
函数
参数 af
与
inet_pton()
函数的
af
参数意义相同。
参数 src
应指向一个
struct in_addr
结构体对象或
struct in6_addr
结构体对象,依据参数
af
而定。函数 inet_ntop()会将参数
src
指向的二进制
IP
地址转换为点分十进制形式的字符串,并将字符串存放在参数
dts 所指的缓冲区中,参数 size
指定了该缓冲区的大小。
inet_ntop()在成功时会返回
dst
指针。如果
size
的值太小了,那么将会返回
NULL
并将
errno
设置为 ENOSPC。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main(void)
{
struct in_addr addr;
char buf[20] = {0};
addr.s_addr = 0xde01a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr: %s\n", buf);
exit(0);
}
socket 编程实战
编写服务器应用程序的流程如下:
①、调用
socket()
函数打开套接字,得到套接字描述符;
②、调用
bind()
函数将套接字与
IP
地址、端口号进行绑定;
③、调用
listen()
函数让服务器进程进入监听状态;
④、调用
accept()
函数获取客户端的连接请求并建立连接;
⑤、调用
read/recv
、
write/send
与客户端进行通信;
⑥、调用
close()
关闭套接字。
下面,我们就根据上面列举的步骤来编写一个简答地服务器应用程序,代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于 5000
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的 IP 地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for ( ; ; ) {
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if(0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
编写客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.1.150" //服务器的 IP 地址
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用 connect 连接远端服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); //端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP 地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if(0 > ret){
perror("send error");
break;
}
//输入了"exit",退出循环
if(0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}