Socket 入门
1. 什么是Socket
1.1. Socket 的引入
我们知道进程通信的方法有管道、命名管道、信号、消息队列、共享内存、信号量等等,这些方法都要求通信的两个进程位于同一个主机。如果通信双方不在同一个主机,使用 TCP/IP 协议族就能达到我们想要的效果。但是,当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展。于是UNIX BSD就发明了socket
1.2. Socket 的位置
socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket 提供了 TCP/IP 协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。简单来说, Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
1.3. Socket 的定义
socket 一词的起源在组网领域的首次使用是在1970年2月12日发布的文献 IETF RFC33 中发现的(第6页),撰写者为 Stephen Carr、Steve Crocker 和 Vint Cerf。根据美国计算机历史博物馆的记载,Croker 写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。(The elements of the name space are called sockets. A socket forms one end of a connection, and a connection is fully specified by a pair of sockets.)”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”
1.4. Socket 的分类
流套接字 (Stream Socket): 主要用于 TCP 协议; 提供了双向的、有序的、无重复的、无记录边界的数据传输服务。
数据报套接字: 主要用于 UDP 协议; 它提供了双向的、无序的、有重复的、有记录边界的数据传输服务。
原始套接字: 主要用于访问底层协议,如 IP、ICMP与IGMP 等协议; 原始套接字可以保存 IP 包中的完整 IP 头部。
1 |
|
1 |
|
2. Socket 常用函数
注意这里的函数都是包含在 C 语言 <socket.h> 和 <type.h> 头文件中的函数
2.1. socket()
1 | int socket(int protofamily, int so_type, int protocol); |
- 点击这里或者查看这篇文章可以查看详情
- protofamily 指协议族,常见的值有:
- AF_INET,指定 so_pcb 中的地址要采用 ipv4 地址类型
- AF_INET6,指定 so_pcb 中的地址要采用 ipv6 的地址类型
- AF_LOCAL/AF_UNIX,指定 so_pcb 中的地址要使用绝对路径名
- so_type 指定 socket 的类型,常见的值有:
- SOCK_STREAM(基于 TCP的,数据传输比较有保障)
- SOCK_DGRAM (是基于 UDP 的,专门用于局域网)
- protocol 指定具体的协议,常见的值有:
- IPPROTO_TCP,TCP 协议
- IPPROTO_UDP,UPD 协议
- 0,如果指定为0,表示由内核根据so_type指定默认的通信协议
值得注意的是,socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而 socket() 用于创建一个 socket 描述符(socket descriptor),它唯一标识一个 socket。这个 socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind() 函数,否则就当调用 connect() 、listen() 时系统会自动随机分配一个端口。
2.2. bind()
1 | int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
- addr:一个
const struct sockaddr *
指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。 - addrlen:对应的是地址的长度。
2.3. listen()
& connect()
1 | int listen(int sockfd, int backlog); |
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
2.4. accept()
1 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
accept()
函数的第一个参数为服务器的socket描述字,第二个参数为指向 struct sockaddr *
的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。
如果 accpet
成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接。
TCP 服务器端依次调用 socket()
bind()
listen()
之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket()
connect()
之后就想 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept()
函数取接收请求,这样连接就建立好了。
之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作.
2.5. read()
& write()
网络I/O操作有下面几组:
1 | read();write(); |
负责相应的数据读写操作
2.6. close()
1 | int close(int fd); |
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的 socket 描述字,好比操作完打开的文件要调用 fclose()
关闭打开的文件。
3. Java Socket 常用 API
3.1. INetAddress 类 —— 套接字地址/域名解析
-
Java使用InetAddress表示IP地址
-
定义于
java.net
包下 -
既可以表示 IPv4 的地址,也可以表示 IPv6 的地址
-
-
在 Java 的 Socket API 中,往往将一个
InetAddress
对象和一个端口号一起使用作为套接字地址 -
通过
InetAddress
的静态方法getByName()
可以将一个IP地址或域名转换为InetAddress
对象- 当对应域名不存在或无法解析时,会抛出
java.net.UnknownHostException
异常,需要对其进行处理
- 当对应域名不存在或无法解析时,会抛出
-
举例如下
1 | import java.net.*; |
3.2. Socket/ServerSocket 类 —— TCP套接字
3.2.1. Socket
Socket 类表示一个建立好的TCP连接,它可以由服务端通过主动连接创建,也可以被 ServerSocket 通过 accept 调用创建(称为主动套接字)
1 | //客户端主动连接 |
3.2.2. ServerSocket
ServerSocket 类表示服务端创建的等待客户端来连接的TCP套接字(称为被动套接字)
1 | //绑定ServerSocket到本地端口并监听 |
3.3. InputStream/OutputStream —— 数据的读取和写出
3.3.1. InputStream
- InputStream类是 Java 提供的输入流抽象
- 定义于
java.io
包下 - 输入流有流终止和传输错误两种情况,而一般而言前者不抛出异常,后者抛出
java.io.IOException
- 通过
read()
方法进行读取
- 定义于
1 | abstract int read()//从输入流读取单个字节,当读取成功时,返回0-255的整数,当流终止时,返回-1 |
通过实现 InputStream 类,可以实现新的输入源,如 FileInputStream,ByteBufferInputStream 等
通过包装 InputStream 类,可以扩充和简化 InputStream的API,如 DataInputStream,BufferedInputStream 等
关于更多的Java IO,可以查看这篇文章
3.3.2 OutputStream
OutputStream类为Java提供的输出流抽象
- 定义于
java.io
包下 - 由于数据流的流终止由调用者决定,因此只存在传输错误一种情况,会抛出
java.io.IOException
异常 - 通过
write()
方法进行写出
1 | abstract void write(int b)//写出单个字节到输出流,只有低8位有效 |
通过实现 OutputStream 类,可以实现新的输出源,如FileOutputStream,ByteBufferOutputStream 等
通过包装 OutputStream 类,可以扩充和简化 OutputStream 的API,如 DataOutputStream,BufferedOutputStream 等
3.4. DatagramSocket/DatagramPacket —— UDP套接字
3.4.1. DatagramSocket
DatagramSocket 类表示一个 UDP 套接字
1 | //绑定UDP套接字到本地端口 |
3.4.2 DatagramPacket
DatagramPacket类封装了一个由UDP套接字传输的数据包,其中包含:
- 一个缓冲区:用于存放UDP报文数据
- 目标机器的IP地址和端口:当DatagramSocket 的 connect 方法被调用后,DatagramPacket 中的目标 IP 和端口将被忽略,直到DatagramSocket 的 disconnect 方法被调用
1 | //创建DatagramPacket的载荷 |
4. 包含 TCP/UDP 的 Socket 使用
4.1. TCP 套接字编程实例
- 客户端从标准输入流读取一行字符串,并将其写出到服务端
- 服务端读取客户端的输入数据,将其转换为大写,并传回给客户端
- 客户端从服务端读取数据,并将转换后的字符串输出到标准输出流,回显给用户
- 如下
1 | package Client |
1 | package Server |
4.2. UDP 套接字编程实例
- 如下
1 | package Client |
1 | package Server |