Socket编程基础
端口号
在前面网络基础部分已经介绍过两台计算机需要通信就需要知道有源IP地址和目标IP地址,有了这一套地址就相当于有了大致方向。实际上计算机之间之所以需要通信,本质上是用户需要通信,所以只有计算机拿到数据还不够,还需要用户获取到数据,而一般情况下,发送方的信息是来自于一个进程的,对应地接收方需要使用一个进程来接收才知道发送方发送的信息,而前面的IP地址只能实现发送方计算机找到接收方计算机,但是接收方计算机收到信息又该发给哪个进程是IP地址无法表示的,如下图所示:
此时就需要一个额外的标识符标记到底是哪一个进程需要接收数据,这个标识符就是端口号
端口号(port)是传输层协议的内容。端口号是一个2字节16位的整数,用来标识一个进程,告诉操作系统当前的这个数据要交给哪一个进程来处理
所以IP地址+端口号能够标识网络上的某一台主机的某一个进程,并且一个端口号只能被一个进程占用,此时的IP+端口号就是socket,也称为套接字
端口号一般有两种划分:
- 0-1023:知名端口号,例如HTTP、FTP、SSH等这些广为使用的应用层协议,他们的端口号都是固定的
- 1024-65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的
因为网络中两个正在通信的计算机实际上是两个正在通信的进程,所以可以理解为网络是两个进程之间共享的数据,两个进程正在做进程间通信
这里提到端口号用于标记当前计算机中每一个进程,但是进程本身也有唯一标识符:进程PID,既然已经有进程PID,为什么还需要使用端口号再进行标识,直接使用进程PID不好吗?主要原因是如果使用进程PID作为网络中的进程唯一标识符,那么此时进程管理和网络管理就强耦合了,一旦修改了其中一种,另外一种就会同时需要改变,所以为了避免增加可维护的难度,就需要在彼此没有关系的层次下建立新的标识符
在传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口
思维链接
实际上,发送方发给接收方信息,接收方接受信息并处理,这对应的就是生产消费模型
认识TCP和UDP协议
了解了系统,也了解了网络协议栈,那么就会清楚传输层是属于内核的,如果要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用来进行的网络通信,示意图如下:
在传输层中存在着两种协议,分别是TCP协议(Transmission Control Protocol传输控制协议)和UDP协议(User Datagram Protocol用户数据报协议),二者特点如下:
=== “TCP协议特点”
1. 传输层协议
2. 有连接
3. 可靠传输
4. 面向字节流(可以不考虑发送方随意接收指定量的数据)
=== “UDP协议特点”
1. 传输层协议
2. 无连接
3. 不可靠传输
4. 面向数据报(发送方发多少就要收多少)
需要注意,在上面的特点中,尽管TCP协议是可靠传输,UDP是不可靠传输,但是实际上在开发中经常会使用二者而不是单纯使用TCP协议,所以「可靠」和「不可靠」并不是协议的缺点或者优点,而更倾向于是特点
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?一般情况下,基本过程为:发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
为使网络程序具有可移植性,使同样的C语言代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32 t htonl(uint32 t hostlong);
uint16 t htons(uint16 t hostshort);
uint32 t ntohl(uint32 t netlong);
uint16 t ntohs(uint16 t netshort);
这些函数名很好记,h
表示host
,n
表示network
,I
表示32位长整数,s
表示16位短整数。例如htol
表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
Socket编程常见API
// 创建socket文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain,int type,int protocol);
// 绑定端口号(TCP/UDP,服务器)
int bind(int socket,const struct sockaddr *address, socklen t address_len);
// 开始监听socket(TCP,服务器)
int listen(int socket,int backlog);
// 接收请求(TCP,服务器)
int accept(int socket,struct sockaddr*address, socklen t*address_len);
// 建立连接(TCP,客户端)
int connect(int sockfd,const struct sockaddr *addr, socklen_t addrlen);
本次不对这些接口进行具体介绍,暂做了解即可
在上面的接口中,会发现存在一个struct sockaddr
的结构,在网络编程中,套接字的种类有下面几种:
- 网络socket(后面主要考虑的socket)
- 本地socket(也称unix域见socket)
- 原始socket(基本淘汰,不作介绍)
所以,实际上使用这些接口时需要考虑到使用网络socket还是本地socket,对应地就存在着两种结构:struct sockaddr_in
(表示网络socket)和struct sockaddr_un
(表示本地socket)
而之所以还需要使用struct sockaddr
是因为这样可以不需要针对同一个功能创建两套接口,只需要保证struct sockaddr_in
和struct sockaddr_un
可以强制转换为struct sockaddr
并在函数中再通过强制转换回到原来的结构就可以实现一套接口完成两种操作,在存在着struct sockaddr
为参数的接口中,一般会有对应的判断,这个判断的作用就是为了区分是struct sockaddr_in
对象还是struct sockaddr_un
,区分的方式就是通过这些结构中共有的第一个字段,这个字段为16位地址类型,而在struct sockaddr_in
对应的就是一个宏值AF_INET
,在struct sockaddr_un
对应的就是另外一个宏值AF_UNIX
。示意图如下:
从上面的基本实现中可以发现,实际上struct sockaddr
和struct sockaddr_in
与struct sockaddr_un
就是抽象类和实现类的关系,这对应的也就是多态