Linux 网络 - 套接字编程
@TOC
零、前言
本章就 Linux 网络编程进行概念及接口学习,并能够简单的进行上手网络套接字编程
一、网络基础知识
1、源 IP 地址和目的 IP 地址
在数据传输时各网络协议栈会对数据进行报头封装,而在 IP 数据包头部中, 有两个 IP 地址, 分别叫做源 IP 地址, 和目的 IP 地址
网络中每台计算机都有一个唯一的 IP 地址,也就是说网络中用 IP 可以标识唯一的一台计算机
数据传输需要知道目标主机,也就是目的 IP;同样的目标主机在收到数据时也需要知道数据是哪一个主机发过来的,也就是源 IP,在目标主机收到消息后也能通过源 IP 对发出数据主机作出响应
2、源 MAC 地址和目的 MAC 地址
大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机
源 MAC 地址和目的 MAC 地址是包含在链路层的报头当中的,而 MAC 地址实际只在当前局域网内有效,因此当数据跨网络到达另一个局域网时,其源 MAC 地址和目的 MAC 地址就需要发生变化
当数据达到路由器时,路由器会对数据进行分发和路由选择,根据源 IP 和目的 IP 进行决定下一个 MAC 地址,此时该数据的源 MAC 地址和目的 MAC 地址就发生了变化
因此数据在传输的过程中是有两套地址:一套是源 IP 地址和目的 IP 地址,这两个地址在数据传输过程中基本是不会发生变化的;另一套就是源 MAC 地址和目的 MAC 地址,这两个地址是一直在发生变化的
3、认识端口号
在实际的传输中,并不是数据在主机中的传输,而是需要将数据传输给对应的进程,所以在数据传输的过程中我们除了需要源 IP 和目的 IP,还需要端口号
从本质上来说,数据的网络传输其实是进程间通信,只不过此时进程间的临界资源变成了网络
端口号(port)是传输层协议的内容,端口号是一个 2 字节 16 位的整数
端口号用来标识主机中的一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程
一个端口号只能被一个进程占用,保证标识进程的唯一性,存在一个进程占有多个端口号,但是一个端口号不能被多个进程占有
4、PORT VS PID
进程 ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念
端口号(port)是网络数据传输中标识主机中进程的唯一性的,它是属于网络的概念
主机中并不是所有的进程都要进行网络通信,大部分的进程是不需要进行网络通信的本地进程,此时 PID 虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了
在不同的场景下可能需要不同的编号来标识某种事物的唯一性,因为这些编号更适合用于该场景
5、TCP 和 UDP 协议
传输层最典型的两种协议就是 TCP 协议和 UDP 协议
TCP 协议
TCP 协议叫做传输控制协议(Transmission Control Protocol),TCP 协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
TCP 协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP 协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP 协议都有对应的解决方法
UDP 协议
UDP 协议叫做用户数据报协议(User Datagram Protocol),UDP 协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议
使用 UDP 协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着 UDP 协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP 协议本身是不知道的
怎么理解协议的可靠与不可靠的
可靠的背后是需要付出代价的,TCP 为了保证数据传输的可靠性需要更加复杂的实现,对应的其数据传输的效率必然会相比于 UDP 有减低
UDP 协议不可靠,但 UDP 协议在底层不需要做过多的工作,且它能够快速的将数据发送给对方,但是风险是数据传输没有保障
编写网络通信代码时具体采用 TCP 协议还是 UDP 协议,完全取决于上层的应用场景:如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用 TCP 协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择 UDP 协议,因为 UDP 协议足够简单
6、网络字节序
我们知道,不同的主机在存储数据时是有大小端之分的,同样的网络数据流也有大端小端之分
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
TCP/IP 协议规定,网络数据流应采用大端字节序,不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据
如果当前发送主机是小端,,就需要先将数据转成大端;否则就忽略,直接发送即可
对于传输的数据计算机底层会自动帮我们做网络字节序的转化,但是在套接字编程时需要填入的一些数据字段是需要我们主动进行网络字节序的转化
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
函数原型:
说明:
这些函数名很好记,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数,例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
二、socket 编程接口
1、sockaddr 结构
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)
因此套接字提供了
sockaddr_in
结构体和sockaddr_un
结构体,其中sockaddr_in
结构体是用于跨网络通信的,而sockaddr_un
结构体是用于本地通信的为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了
sockeaddr
结构体,该结构体与sockaddr_in
和sockaddr_un
的结构都不相同,但这三个结构体头部的 16 个比特位都是一样的,这个字段叫做协议家族函数接口会根据结构体头部 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 结构体指针做为参数
sockaddr_in 结构和 in_addr 结构:
注意:
虽然 socket api 的接口是 sockaddr,但是我们真正在基于 IPv4 编程时,使用的数据结构是 sockaddr_in
sockaddr_in 结构里主要有三部分信息:地址类型,端口号,IP 地址
in_addr 用来表示一个 IPv4 的 IP 地址,其实就是一个 32 位的整数
2、socket 常见 API
函数原型:
三、UDP 套接字
1、创建套接字
无论是服务端还是客户端,进行网络编程需要做的第一件事就是创建套接字
socket 函数函数原型:
解释:
domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。填写
struct sockaddr
结构的前 16 位:本地通信设置为AF_UNIX
,网络通信设置为AF_INET
(IPv4)或AF_INET6
(IPv6)type:套接字协议的传输类型:对于 UDP 的数据报式传输则填入
SOCK_DGRAM
,对于 TCP 的流式传输则填入SOCK_STREAM
protocol:创建套接字的协议类别。可以指明为 TCP 或 UDP,但该字段一般直接设置为 0 就可以了,即默认(会根据前两个参数自动推导)
返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置
示例:
2、填写 ip/port 和绑定
对于服务端和客户端都要进行绑定 ip 及 port,只有绑定后才能标识网络中唯一的主机中的进程服务,便于进程接下来的数据传输
struct sockaddr_in
成员:
sin_family:表示协议家族
sin_port:表示端口号,是一个 16 位的整数
sin_addr:表示 IP 地址,是一个 32 位的整数
sin_addr 中的成员 s_addr:表示 IP 地址,是一个 32 位的整数
注意:
对于服务端来说,服务端 ip 和 port 需要被多个客户端所熟知的,所以服务端的 port 是需要进行固定化的,也就是说一个服务端的 port 是该服务端所私有的,不能随意更换
对于云服务器上的服务端,不建议绑定明确的 ip,建议使用 INADDR_ANY 绑定该主机所有设备,以此接收向该主机发送的所有数据
对于客户端来说,客户端是不提供服务的,ip 和 port 不用被其他主机熟知,并且为了启动客户端的顺利(固定的 port 被占用会使得进程启动不了),所以不需要我们主动去进行绑定 ip 和 port,当进行数据的发送时,系统会自动绑定 ip 以及随机的 port
对于客户端虽然不用主动填写自己的 ip 和 port,但是需要的是明确数据传输的主机中的进程,即需要填写服务端的 ip 和 port
IP 格式转化:
对于进行绑定的网络信息字段是需要我们主动进行网络字节序的转化的,系统提供了相应的接口(上面介绍了),而发送的数据系统会在底层进行网络字节序的转化
在 ip 的转化时,我们习惯用的是点分十进制的字符串 ip,例如 192.168.233.123,但是需要填入的 ip 形式是四字节整数 ip
inet_addr 函数的函数原型:
解释:
功能:将点分十进制的字符串 IP 转换成四字节整数 IP
传入待转换的字符串 IP,该函数返回的就是转换后的整数 IP
inet_ntoa 函数原型:
解释:
将四字节整数 IP 转换成点分十进制字符串 IP
传入 inet_ntoa 函数的参数类型是
in_addr
,不需要选中in_addr
结构当中的 32 位的成员传入,直接传入in_addr
结构体即可
服务端创建套接字,即底层打开了对应的网络套接字文件,想进行网络通信还需要绑定对应的网络信息,即将套接字文件与网络进行强相关
bind 函数函数原型:
解释:
sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符
addr:网络相关的属性信息,包括协议家族、IP 地址、端口号等
addrlen:传入的 addr 结构体的长度
返回值说明:绑定成功返回 0,绑定失败返回-1,同时错误码会被设置
注意:
在绑定时需要将网络相关的属性信息填充到 struct sockaddr_in 结构体当中,然后将该结构体地址作为 bind 函数的第二个参数进行传入(这里需要强转为 struct sockaddr *addr 类型)
UDP 是数据报式套接字,并不会管对端的接收转态,只要绑定后就可以向对端进行接收消息了,但是这样的传输实际中是存有风险的
示例:服务端
示例:客户端
3、数据发送和接收
sendto 函数原型:
解释:
sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
buf:待写入数据的存放位置
len:期望写入数据的字节数
flags:写入的方式,一般设置为 0,表示阻塞写入
dest_addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等
addrlen:传入 dest_addr 结构体的长度
返回值说:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
注:由于 UDP 不是面向连接的,所以传输数据时需要指明对端网络相关的信息,即 sendto 的最后两个参数用来表示对端的信息
recvfrom 函数函数原型:
解释:
sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据
buf:读取数据的存放位位置
len:期望读取数据的字节数
flags:读取的方式,一般设置为 0,表示阻塞读取
src_addr:对端网络相关的属性信息,包括协议家族、IP 地址、端口号等
addrlen:调用时传入期望读取的 src_addr 结构体的长度,返回时代表实际读取到的 src_addr 结构体的长度,这是一个输入输出型参数
返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置
注:recvfrom 接口的倒数第二个参数是一个输出型参数,用于获取发送消息的对端网络信息,这样就知道是谁发的数据,并可以进一步向对端做出回应
示例:服务端
示例:客户端
4、简单回声服务器
写一个简单的回声服务器:
当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用 sento 函数将收到的数据重新发送给对应的客户端,以此测试双端的数据的收发功能
服务端代码:
客户端代码:
运行效果:
版权声明: 本文为 InfoQ 作者【可口也可樂】的原创文章。
原文链接:【http://xie.infoq.cn/article/04b3f836f8409d742981c6f59】。文章转载请联系作者。
评论