初识Linux套接字(socket)和TCP/UDP协议
这是系列博客中的第二篇,导航如下
在Linux环境中,套接字(Socket)是一种用于进程间通信
(IPC)的机制,但这里的进程,包括了同一网络下其它主机的进程,所以它被广泛应用于网络编程。它允许不同计算机上的进程或同一计算机上的不同进程之间进行数据交换。
由于此时我们的网络编程基础较少,所以本文的内容更偏向于实践,而不是原理。希望能在动手实践中加深对网络编程的熟悉程度,减少陌生感
在本文中,我们将:
知识铺垫
:认识IP地址, 端口号, 网络字节序等网络编程中的基本概念(简略)socket API学习
- 实现一个简单的UPD
客户端
/服务器
- 实现一个简单的TCP
客户端
/服务器
(服务器包括单连接版本,多进程版本,多线程版本)
知识铺垫
理解源IP地址和目的IP地址
在数据包的头部中,包含两个IP地址,分别叫做源IP地址
和目的IP地址
为什么要有两个IP地址?因为一般通过网络
建立的通信都是双向的,而且一方接收请求后,一般还要把响应发送回去,所以含有两个IP地址才能方便地建立双向链接和通信。而且数据包每经过一个中间主机,都会被询问一次源和目的,就像唐僧常说的口头禅:“贫僧自东土大唐(源IP)
而来,要到西天(目的IP)
取经去”。
总结:通过IP协议
,我们能找到唯一的主机建立网络通信。
认识端口号
套接字(Socket)是一种用于进程间通信
(IPC)的机制,然而IP协议
只能指定唯一主机,想要进程间通信明显还不够。那在一台主机上,怎么标识唯一的进程呢?端口号(port)
应运而生。
- 端口号是一个2字节16位的整数
- 正如上文所说,端口号用来标识本机的唯一进程
互斥性
:为保证唯一性,一个端口号只能被一个进程占用
综上,通过IP地址
+端口号
便能够唯一地表示网络上的某一台主机的某一个进程。
端口号 与 进程ID 辨析
既然端口号port
和进程ID
都可以唯一地标识一个进程,那么这两者有何相似与差异呢?
进程ID
:一个线程对应一个pid
,一个pid
对应一个线程,且操作系统就是用pid
来调度线程的端口(port)
:端口号更像是一种被所有线程共享的互斥资源,每个都只有一份,但是同一个线程可以申请占用多个端口号,而一个端口号因为只有一份,只能被一个线程占用
源端口号和目的端口号
和源IP地址和目的IP地址配套使用,用于建立两台主机上特定的两个线程间的通信。
认识TCP协议和UDP协议
这里仅仅是有一个大概的了解,对协议原理更详细的较少将在后面的博客中提出
TPC协议
TCP(Transmission Control Protocol)是一种可靠的传输协议,特性如下
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议
UDP(User Datagram Protocol)是一种不可靠的传输协议,特性如下
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,
网络数据流同样有大端小端之分,这给网络中不同主机的通信带来了困难, 那么如何定义网络数据流的地址,以保证不同机器能通过网络通信呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
关于网络字节序的规则如上,显然这些都由程序员来做,效率还是太低了。
为了提高编程效率,同时也为了提高代码移植性,使同样的C代码在大端计算机和小端计算机上都能正常运行,可以调用以下头文件的库函数做网络字节序和主机字节序的转换
1 |
|
- 这些函数名很好记,
h
表示host
,n
表示network
,l
表示32位长整数
,s
表示16位短整数
。 - 例如
htons
表示将16位的短整数从主机字节序转换为网络字节序,例如将port端口号转换后准备发送 - 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
Socket编程
<sys/socket.h>
提供了一系列套接字相关的接口,用于进行网络通信服务
基于Linux中一切皆文件
的祖训,在套接字编程所创建的socket文件
,也是作为文件管理的,socket文件
也会申请自己的文件描述符
,和其它打开的文件一起被管理。
1 |
|
常用接口如上,具体使用方法稍后在代码中展示
struct sockaddr*介绍
为什么不直接介绍struct sockaddr
struct sockaddr是个很简单的结构体,具有类似如下的结构
1 | struct sockaddr |
然而实际上在函数里用的也不是struct sockaddr
,而是其它成员变量更丰富的结构体
它唯一的用处就是在函数传参时防止类型不匹配导致的报错,函数内部如何处理指针指向的内存,取决于sa_family
的值。struct sockaddr*
指针的作用有点类似于面向对象中父类指针在函数传参中的作用
各种sockaddr_
家族
1 | //使用ipv4地址 |
用途
接下来的介绍以struct sockaddr_in
为例
1 | struct sockaddr_in { |
struct sockaddr*
指向的结构体的用处对于服务器/客户端有所不同
- 对服务器提供服务
- 规定提供网络服务的ip格式(ipv4/ipv6)
- 规定提供网络服务的ip(因为一个电脑可以有多个ip,后文解释)
- 规定提供网络服务的端口号值
- 对服务器连接客户端
- 储存客户端的ip地址
- 储存客户端的port端口
- 对客户端
- 存储网络消息源的信息
in_addr
的底层结构
1 | typedef uint32_t in_addr_t; |
in_addr
用来表示一个IPv4的IP地址. 其实就是一个32位的无符号整数;
UDP通信编程
使用接口
int socket(int domain, int type, int protocol);
- 用于创建套接字文件,获取套接字文件描述符
fd
domain
规定域名通信协议,比如IPV4
、IPV6
、本地通信协议
等type
通信类型,SOCK_DGRAM
表示面向数据报
,SOCK_STREAM
表示面向字节流,以及更多的类型protocol
协议代码,可选内容取决于domain
参数,可为0
返回值
为套接字文件的文件描述符
- 用于创建套接字文件,获取套接字文件描述符
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 用于给指定
sockfd
绑定网络通信协议 sockfd
文件描述符addr
就是上面介绍的struct sockaddr
的子类addrlen
为addr
指向结构体的大小返回值
成功则返回0
,失败则返回1
- 用于给指定
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
文件描述符buf
字节串缓冲区,用于存放接收到的字节流len
缓冲区大小,防止越界访问flags
为位图,可以传多个参数。本次介绍不传参,所以传入一个0
src_addr
指向储存消息源信息的结构体地址addrlen
传入结构体的大小
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
文件描述符buf
字节流缓冲区,用于存放待发送的字节串len
待发送字节串的长度flags
为位图,可以传多个参数。本次介绍不传参,所以传入一个0
src_addr
指向储存目标信息的结构体地址addrlen
传入结构体的大小