1. Socket简介
Socket即套接字,它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上连应用进程,下连网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口 。
1.1 工作流程
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :
(1)服务器监听
(2)客户端请求
(3)连接的确认
1.1.1 服务器监听
服务器监听是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。
1.1.2 客户端请求
客户端请求是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口,然后就向服务器端套接字提出连接请求 。
1.1.3 连接的确认
连接的确认是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求 。
1.2 主要特点
根据套接字的不同类型,可以将套接字调用分为面向连接服务和面向无连接服务。
1.2.1 面向连接的服务
(1)数据传输过程必须经过建立连接、维护连接和释放连接三个阶段
(2)在传输过程中,各分组不需要携带目的主机的地址
(3)可靠性好,但由于协议复杂,通信效率不高
1.2.2 面向无连接服务
(1)不需要连接的各个阶段
(2)每个分组都携带完整的目的主机地址,在系统中独立传送
(3)由于没有顺序控制,所以接收方的分组可能出现乱序、重复和丢失现象
(4)通信效率高,但可靠性不能确保
1.3 地址端口
地址即所谓的IP地址,网络是由很多终端组合在一起的集合,如何能够在众多的终端中确定唯一终端是通过IP地址来完成的。就像在中国这个人口大国中,你的身份信息是通过身份证号来确定的,我可以通过身份证号来获取你的信息找到你。
端口是用来区分终端上的应用。在一个终端上会有很多的应用,比如你会在自己的电脑上安装QQ、微信、钉钉等应用程序,此时端口就起到了类似寻址的作用,我可以通过端口来找到指定的应用程序。
2. TCP与UDP
2.1 TCP协议
TCP(Transmission Control Protocol,传输控制协议)是 面向连接的协议 ,即在收发数据时都需要建立可靠的连接,即TCP的 三次握手 建立连接以及TCP的 四次挥手 断开连接。建立一个TCP连接时,需要客户端和服务端总共发送三个包以确认连接的建立, 在Socket编程中,这一过程由客户端执行connect来触发。
2.1.1 三次握手
客户端与服务端建立连接需要进行三次握手
- 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server, Client进入SYN_SENT状态,等待Server确认。
- 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位 SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求 ,Server进入SYN_RCVD状态。
- 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK 置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则 连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以 开始传输数据了。
2.1.2 四次挥手
客户端与服务端断开连接需要进行四次挥手
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入 FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1,Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK 状态。
- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
同时发起主动关闭的情况
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
2.2 UDP协议
UDP (User Datagram Protocol,用户数据报协议) 是 面向无连接 的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制。在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。相比TCP就是无需建立连接,结构简单,无法保证正确性,容易丢包。
3. JAVA类支持
针对不同的网络通信层次,Java给我们提供的网络功能有四大类:
- InetAddress: 用于标识网络上的硬件资源
- URL: 统一资源定位符,通过URL可以直接读取或者写入网络上的数据
- Socket和ServerSocket: 使用TCP协议实现网络通信的Socket相关的类
- Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信
3.1 InetAddress测试代码
package com.intest.test;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @ClassName InetAddressTest
* @Description 资源测试类
* @Author 周威
* @Date 2020-10-14 - 下午 3:13
*/
public class InetAddressTest
{
public static void main(String[] args) throws UnknownHostException
{
// 获取本机信息
InetAddress localHost = InetAddress.getLocalHost();
// 获取本机地址
String hostAddress1 = localHost.getHostAddress();
System.out.println("hostAddress1 = " + hostAddress1);
// 获取本机名称
String hostName1 = localHost.getHostName();
System.out.println("hostName1 = " + hostName1);
// 获取百度信息
InetAddress baiduHost = InetAddress.getByName("www.baidu.com");
// 获取百度地址
String hostAddress2 = baiduHost.getHostAddress();
System.out.println("hostAddress2 = " + hostAddress2);
// 获取百度名称
String hostName2 = baiduHost.getHostName();
System.out.println("hostName2 = " + hostName2);
}
}
3.2 InetAddress测试结果
4. 基于TCP协议的Socket通信
4.1 TCP通信模型
4.2 服务端开发
4.2.1 开发步骤
(1)创建ServerSocket对象,并绑定端口
(2)通过accept()方法监听客户端连接
(3)获取连接输入输出流,通过流来获取客户端请求和发送客户端响应
(4)关闭连接资源
4.2.2 开发代码
package com.intest.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName SocketServer
* @Description Socket服务端
* @Author 周威
* @Date 2020-10-14 - 上午 10:14
*/
public class SocketServer
{
public static void main(String[] args) throws IOException
{
// 创建ServerSocket对象,并绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
// 监听客户端连接
Socket socket = serverSocket.accept();
// 获取连接输入输出流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 读取客户端请求数据
int length = -1;
byte[] buffer = new byte[1024*5];
while ((length = inputStream.read(buffer)) != -1)
{
String request = new String(buffer, 0, length);
// 打印客户端请求数据
System.out.println("request = " + request);
// 发送客户端响应数据
String response = "你好,客户端";
outputStream.write(response.getBytes());
}
// 关闭输入输出流
outputStream.close();
inputStream.close();
// 关闭连接资源
socket.close();
serverSocket.close();
}
}
4.3 客户端开发
4.3.1 开发步骤
(1)创建Socket对象,绑定服务地址与端口
(2)获取连接输入输出流,通过流来发送请求和获取服务端响应
(3)关闭连接诶资源
4.3.2 开发代码
package com.intest.tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @ClassName SocketClient
* @Description Socket客户端
* @Author 周威
* @Date 2020-10-14 - 上午 10:31
*/
public class SocketClient
{
public static void main(String[] args) throws IOException
{
// 创建Socket对象,绑定服务地址与端口
Socket socket = new Socket("127.0.0.1", 8888);
// 获取连接输入输出流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 发送客户端请求数据
String request = "我是客户端";
outputStream.write(request.getBytes());
// 读取服务端响应数据
int length = -1;
byte[] buffer = new byte[1024];
while ((length = inputStream.read(buffer)) != -1)
{
String response = new String(buffer, 0, length);
// 打印服务端响应数据
System.out.println("response = " + response);
}
// 关闭输入输出流
inputStream.close();
outputStream.close();
// 关闭连接资源
socket.close();
}
}
4.4 运行测试
- 首先运行服务端程序
- 其次运行客户端程序
4.4.1 服务端获得请求
4.4.2 客户端获得响应
5. 基于UDP协议的Socket通信
5.1 服务端开发
package com.intest.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* @ClassName SocketServer
* @Description Socket服务端
* @Author 周威
* @Date 2020-10-14 - 下午 1:16
*/
public class SocketServer
{
public static void main(String[] args) throws IOException
{
// 创建DatagramSocket对象,绑定指定端口
DatagramSocket socket = new DatagramSocket(8888);
// 接收客户端请求数据报
byte[] sourceData = new byte[1024];
DatagramPacket sourcePacket = new DatagramPacket(sourceData, sourceData.length);
socket.receive(sourcePacket);
String request = new String(sourceData, 0, sourcePacket.getLength());
System.out.println("request = " + request);
// 发送服务端响应数据报
InetAddress address = sourcePacket.getAddress();
int port = sourcePacket.getPort();
String response = "你好客户端";
byte[] targetData = response.getBytes();
DatagramPacket targetPacket = new DatagramPacket(targetData, targetData.length, address, port);
socket.send(targetPacket);
// 关闭资源
socket.close();
}
}
5.2 客户端开发
package com.intest.udp;
import java.io.IOException;
import java.net.*;
/**
* @ClassName SocketClient
* @Description Socket客户端
* @Author 周威
* @Date 2020-10-14 - 下午 1:17
*/
public class SocketClient
{
public static void main(String[] args) throws IOException
{
// 定义服务端地址与端口
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 8888;
// 发送客户端请求数据报
String request = "我是客户端";
byte[] targetData = request.getBytes();
DatagramPacket targetPacket = new DatagramPacket(targetData, targetData.length, address, port);
DatagramSocket socket = new DatagramSocket();
socket.send(targetPacket);
// 接收服务端响应数据报
byte[] sourceData = new byte[1024];
DatagramPacket sourcePacket = new DatagramPacket(sourceData, targetData.length);
socket.receive(sourcePacket);
String response = new String(sourceData, 0, sourcePacket.getLength());
System.out.println("response = " + response);
// 关闭资源
socket.close();
}
}
5.3 运行测试
- 首先运行服务端程序
- 其次运行客户端程序