跳转至

Socket编程基础

约 2553 个字 21 行代码 7 张图片 预计阅读时间 9 分钟

端口号

在前面网络基础部分已经介绍过两台计算机需要通信就需要知道有源IP地址和目标IP地址,有了这一套地址就相当于有了大致方向。实际上计算机之间之所以需要通信,本质上是用户需要通信,所以只有计算机拿到数据还不够,还需要用户获取到数据,而一般情况下,发送方的信息是来自于一个进程的,对应地接收方需要使用一个进程来接收才知道发送方发送的信息,而前面的IP地址只能实现发送方计算机找到接收方计算机,但是接收方计算机收到信息又该发给哪个进程是IP地址无法表示的,如下图所示:

此时就需要一个额外的标识符标记到底是哪一个进程需要接收数据,这个标识符就是端口号

端口号(port)是传输层协议的内容。端口号是一个2字节16位的整数,用来标识一个进程,告诉操作系统当前的这个数据要交给哪一个进程来处理

所以IP地址+端口号能够标识网络上的某一台主机的某一个进程,并且一个端口号只能被一个进程占用,但是一个进程可以绑定多个端口号,此时的IP+端口号就是socket,也称为套接字

端口号一般有两种划分:

  1. 0-1023:知名端口号,例如HTTP、FTP、SSH等这些广为使用的应用层协议,他们的端口号都是固定的
  2. 1024-65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的

因为网络中两个正在通信的计算机实际上是两个正在通信的进程,所以可以理解为网络是两个进程之间共享的数据,两个进程正在做进程间通信

这里提到端口号用于标记当前计算机中每一个进程,但是进程本身也有唯一标识符:进程PID,既然已经有进程PID,为什么还需要使用端口号再进行标识,直接使用进程PID不好吗?主要原因是如果使用进程PID作为网络中的进程唯一标识符,那么此时进程管理和网络管理就强耦合了,一旦修改了其中一种,另外一种就会同时需要改变,所以为了避免增加可维护的难度,就需要在彼此没有关系的层次下建立新的标识符

在传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口

知识链接:

实际上,发送方发给接收方信息,接收方接受信息并处理,这对应的就是生产消费模型

常见的知名端口号

  1. SSH服务器,使用22端口
  2. FTP服务器,使用21端口
  3. Telnet服务器,使用23端口
  4. HTTP服务器,使用80端口
  5. HTTPS服务器,使用443端口

认识TCP和UDP协议

了解了系统,也了解了网络协议栈,那么就会清楚传输层是属于内核的,如果要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用来进行的网络通信,示意图如下:

在传输层中存在着两种协议,分别是TCP协议(Transmission Control Protocol传输控制协议)和UDP协议(User Datagram Protocol用户数据报协议),二者特点如下:

  1. 传输层协议
  2. 有连接
  3. 可靠传输
  4. 面向字节流(可以不考虑发送方随意接收指定量的数据)
  1. 传输层协议
  2. 无连接
  3. 不可靠传输
  4. 面向数据报(发送方发多少就要收多少)

需要注意,在上面的特点中,尽管TCP协议是可靠传输,UDP是不可靠传输,但是实际上在开发中经常会使用二者而不是单纯使用TCP协议,所以「可靠」和「不可靠」并不是协议的缺点或者优点,而更倾向于是特点

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?一般情况下,基本过程为:发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可

为使网络程序具有可移植性,使同样的C语言代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

C
1
2
3
4
5
#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表示hostn表示networkI表示32位长整数,s表示16位短整数。例如htol表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回

Socket编程常见API

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 创建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的结构,在网络编程中,套接字的种类有下面几种:

  1. 网络socket(后面主要考虑的socket)
  2. 本地socket(也称unix域见socket)
  3. 原始socket(基本淘汰,不作介绍)

所以,实际上使用这些接口时需要考虑到使用网络socket还是本地socket,对应地就存在着两种结构:struct sockaddr_in(表示网络socket)和struct sockaddr_un(表示本地socket)

而之所以还需要使用struct sockaddr是因为这样可以不需要针对同一个功能创建两套接口,只需要保证struct sockaddr_instruct sockaddr_un可以强制转换为struct sockaddr并在函数中再通过强制转换回到原来的结构就可以实现一套接口完成两种操作,在存在着struct sockaddr为参数的接口中,一般会有对应的判断,这个判断的作用就是为了区分是struct sockaddr_in对象还是struct sockaddr_un,区分的方式就是通过这些结构中共有的第一个字段,这个字段为16位地址类型,而在struct sockaddr_in对应的就是一个宏值AF_INET,在struct sockaddr_un对应的就是另外一个宏值AF_UNIX。示意图如下:

从上面的基本实现中可以发现,实际上struct sockaddrstruct sockaddr_instruct sockaddr_un就是抽象类和实现类的关系,这对应的也就是多态

网络常见指令

ping命令

ping命令用于查看指定的域名或者IP地址是否可以与本机连通,基本使用方式如下:

Bash
1
ping IP地址/域名

例如:

但是直接使用ping命令会发现是死循环地执行,如果一般查看是否连通只需要ping1次就足够了,所以ping命令还可以指定ping的次数,基本使用如下:

Bash
1
ping -c 次数 IP地址/域名

例如:

netstat命令

netstat是一个用来查看网络状态的重要工具,常见的选项有:

  • n拒绝显示别名,能显示数字的全部转化成数字
  • l仅列出有在Listen(监听)的服務状态
  • p显示建立相关链接的程序名和对应进程PID
  • t仅显示TCP相关选项
  • u(UDP)仅显示UDP相关选项
  • a(all)显示所有选项,默认不显示LISTEN相关

Note

如果是以普通用户使用netstat -p命令可能在PID/Program name中看到-,这是因为部分服务不允许普通用户查看和绑定,想要查看就需要使用sudo

例如:

有的时候可能需要一直查看相关的状态,此时就可以使用watch命令间隔一段时间执行指定命令,这个命令会根据用户执行的间隔时间周期性得执行一次指定的指令,基本使用方式如下:

Bash
1
watch -n 间隔时间(以秒为单位) 具体指令

例如:

Bash
1
watch -n 1 ls

上面的图中展示了每隔1s执行一次ls命令

pidof命令

返回指定正在运行的程序的PID,基本使用方式如下:

Bash
1
pidof 指定正在运行的程序

在终止一个进程时,有时需要使用kill -9命令终止一个进程,此时需要指定终止的进程的PID,那么就可以结合pidof命令使用。但是如果分开使用两个命令,那么使用方式和之前使用ps命令没有区别,所以可以考虑使用管道结合xargs命令,例如:

Bash
1
pidof 正在运行的程序名 | xargs kill -9

上面的命令表示执行pidof并将返回结果交给管道,通过管道将结果交给xargs,这个命令会将收到的结果作为kill -9的命令行参数传递给kill -9