Socket
本篇尝试尽可能的介绍清除Socket
什么是Socket
Socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,
它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,
一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
简单的说Socket就是计算机之间使用一种约定俗成的方式进行数据沟通的接口。
Socket在哪里使用
我们简单的知道了Socket的作用,现在有一个问题,什么时候我们会使用它。解决这个疑问我们就需要简单了解下
TCP/IP。
TCP/IP
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
在这里Socket是应用层与TCP/IP协议族通信的中间软件的抽象层。提供了接口服务。TCP/IP协议族中Socket所在的位置
Socket的使用流程
整个Socket的使用分为两部分
服务端:
- 服务端初始化Socket
- 服务端绑定端口,对端口进行监听
- 使用acceot进行阻塞,等待连接
- 接收客户端连接
- 接收客户端消息
- 返回回应数据
- 关闭连接
客户端
- 初始化Socket
- 连接服务器IP、端口
- 连接成功则和服务端建立连接
- 客户端发送请求到服务端
- 接收服务端返回的数据
- 关闭连接
网络通讯
网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样我们使用“IP + 协议 + 端口”就可以在网络中标识唯一的进程了。然后我们使用socket接口实现网络进程之间的通信
socket起源于Unix,Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
Socket能实现什么
socket 的典型应用就是 Web 服务器和浏览器:
- 浏览器获取用户输入的 URL,向服务器发起请求,
- 服务器分析接收到的 URL,将对应的网页内容返回给浏览器,
- 浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
JAVA中Socket如何使用
Socket的简单使用
创建服务端
代码
public static void main(String[] args) throws IOException {
baseSocketServer();
}
/**
* 基础是Socket案例
*/
public static void baseSocketServer() {
int port = 8000;
try (ServerSocket socket = new ServerSocket(port);
// server将一直等待连接的到来
Socket accept = socket.accept();
// 获取Socket的输入流,用来获得客户端的数据
InputStream inputStream = accept.getInputStream()) {
// 解析收到的消息
String message = getMessage(inputStream);
System.out.println("get message from client: " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对输入流进行解析获得返回的内容
* @param inputStream
* @return
* @throws IOException
*/
private static String getMessage(InputStream inputStream) throws IOException {
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
//注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len,"UTF-8"));
}
return sb.toString();
}
服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务
端口的绑定(此处源码解读)
服务器绑定端口的时候其实绑定了本地的IP和端口
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
在使用bind
方法的时候获取InetSocketAddressHolder
地址使用了默认本地IP
public InetSocketAddress(InetAddress addr, int port) {
holder = new InetSocketAddressHolder(
null,
addr == null ? InetAddress.anyLocalAddress() : addr,
checkPort(port));
}
创建客户端
public static void main(String[] args) throws Exception {
baseSocketClient();
}
/**
* 基础的Socket客户端
* @throws IOException
*/
public static void baseSocketClient() throws IOException {
// 监听地址和端口
SocketAddress address = new InetSocketAddress("127.0.0.1", 8000);
try (Socket socket = new Socket()) {
socket.connect(address);
try (// 获取输入输出流,读写数据(与服务端数据读写操作相同)
OutputStream out = socket.getOutputStream();
// 获取输入流
InputStream in = socket.getInputStream()){
// 写数据
out.write("hello, server".getBytes());
// 关闭此处输出流
socket.shutdownOutput();
// 解析服务器返回的内容
String message = getMessage(in);
// 获得返回的内容
System.out.println("Server return:" + message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对输入流进行解析获得返回的内容
* @param inputStream
* @return
* @throws IOException
*/
private static String getMessage(InputStream inputStream) throws IOException {
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
//注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len,"UTF-8"));
}
return sb.toString();
}
这样我们先启动服务端内容,然后启动客户端就可以实现一次数据通讯。
socket连接建立
三次握手(three times handshake;three-way handshake)所谓的“三次握手”即对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。
三次握手分别是:
- 客户端发送syn包到服务器,进入SYN_SENT状态,等待服务端返回
- 服务端收到syn包,确认客户的SYN,同时自己发送一个SYN,即SYN+ACK,此时服务器进入SYN_RECV
- 客户端收到服务端的SYN+ACK,向服务器发送确认包ACK此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态
A[客户端-CLOSED]–>B
B[客户端-SYN-SENT-等待服务器确认]–>C
C[客户端-ESTABLISHED-完成连接]
E[服务端-CLOSED]–>F
F[服务端-LISTEN]–>G
G[服务端-SYN-RECEIVED-等待确认]–>H
B–>|SYN=1 SEQ=X ACK=0|F
H[服务端-ESTABLISHED]
G–>|SYN=1 SEQ=Y ACK=X+1|C
C–>|SEQ=X+1 ACK=Y+1|H
socket连接的关闭
四次挥手,别名连接终止协议。其性质为终止协议
连接的关闭需要四次挥手,操作流程分别是:
- 客户端发送一个FIN,主动关闭服务器数据传输。
- 服务器端收到FIN,返回一个ACK,确认序号为收到的序号加1。
- 服务器关闭客户端的连接,发送一个FIN给客户端。
- 客户端发回ACK报文确认,并将确认序号设置为收到序号加1。
TCP 用三步来建立一个链接,而终止连接时需要四步。原因在于被动关闭链接一方需要关闭处理时间,因此 ACK 和 FIN 不能同时发给主动关闭一方。
连接的状态
状态 | 描述 |
---|---|
CLOSED | 初始状态 |
LISTEN | 服务端处于监听状态,可以接受连接 |
SYN_RCVD | 握手状态:接受了SYN报文,等待客户端ACK报文 |
SYN_SENT | 握手状态:客户端执行连接操作,发送SYN报文,等待服务端消息 |
ESTABLISHED | 连接已经 |
FIN_WAIT_1 | 客户端发送了FIN报文,等待服务端回复 |
FIN_WAIT_2 | 客户端发送了FIN报文,服务端已经回复 |
TIME_WAIT | 收到了对方的FIN报文,并发送出了ACK报文,等2MSL后返回CLOSED可用状态 |
CLOSE_WAIT | 收到对方的FIN报文,此次返回ACK报文给对方,然后等待数据传输完毕后进行关闭之间的状态 |
LAST_ACK | 发送确认关闭的FIN报文,等待对方返回ACK |
CLOSING | 如果双方尝试同时close一个Socket,同时发送FIN报文,会出现此状态 |