socket 是什么
要介绍网络编程套接字和 soket 首先我们要知道 TCP / IP 5层网络模型,在这个网络模型中我们的应用程序在最上面的应用层;操作系统作用在第二层传传输层和第三层网络层;驱动程序在第四层数据链路层;网络相关的一些硬件,如网卡等,在第五层物理层。 soket 是一种广义的文件,相当于操作系统对网卡一类的硬件进行的扩充,我们对 socket 文件进行操作,实际上就是在对网卡等硬件设备进行操作,网络编程套接字 就是为了方便我们对 socket 文件进行操作,传输层协议提供给应用层使用的API ,也叫作 socket API。
传输层协议中,最主要的就是 TCP 和 UDP,这两个协议存在很大的差异,因此两组协议提供的 soket API 也存在很大的差异,这里我会通过两个简单的回显服务代码对两组 API 进行简单的介绍。回显服务指的是服务器不对客户端传过来的数据进行任何操作,直接将客户端的请求数据作为响应,原封不动的传回给客户端。但在实际的客户端、服务器交互中,服务器计算响应是我们主要实现的部分,这里只是介绍 soket API,所以不对服务器功能进行设计,直接使用最简单的回显服务。
UDP回显服务
首先,我们先来看一下 UDP 提供的 soket API 的两个主要的类。
DatagramSocket;
DatagramPacket;
这两个类虽然没有写明 UDP ,但我们知道, UDP协议传输数据时,采用面相数据报的方式,而Datagram 就是数据报的意思,这也意味着,使用 UDP 协议进行数据传输时,数据必须是一个一个数据报的形式,我们就可以使用DatagramPacket 这个类提供的方法将数据打包成数据报,用DatagramSocket 中的方法解析数据报中的数据,再对这些数据进行各种操作。下面就通过 UDP 回显服务的服务器以及客户端代码对这组 API 进行简单的介绍。
服务器代码
public class UDPEchoServer {
private DatagramSocket socket = null;
public UDPEchoServer(int port) throws SocketException {
// 1. 由于 socket 对象在创建时存在失败的可能,所以我们需要处理异常
// 2. 客户端和服务器的交互需要明确彼此的IP地址以及端口号
// 一般会给服务器指定一个端口号,客户端则采用系统分配的端口
// 因为服务器的端口不明确,客户端就不知道数据要传到哪里,而客户端传输的数据中会包含自身的IP和端口
// 并且客户的机器上有哪些端口被占用了是不确定的,系统给客户端分配端口可以避免端口冲突
// 在构造方法中指定服务器端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
// 在一些步骤中用输出做一些提示
System.out.println("服务器启动");
while(true) {
// 服务器启动后,循环读取数据并进行响应
// 1. 构造 DatagramPacket 对象时,参数传入字节数组,以及一个数据报的最大长度
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
// 2. socket 调用 receive 方法,将空的 packet 作为参数传入,再将接收到的数据写入到 packet 中
socket.receive(packet);
// 3. 将写好数据的 packet 构造成字符串
// 三个参数分别表示 获取请求数据,开始设置的下标,需要设置的长度
String request = new String(packet.getData(), 0, packet.getLength());
// 为了验证服务器响应效果,可以分别打印一下请求字符串和响应字符串
System.out.println(request);
// 4. 将请求字符串作为参数传输给计算响应的函数,然后将返回值设置成响应字符串
String response = process(request);
System.out.println(response);
// 5. 对返回的响应数据进行解析,并存入到数据报中
// 这里的三个参数表示 将字符串转成字节数组,将数据报长度置为字节数组长度,获取客户端的IP以及端口号
// 需要注意的是长度设置一定是response.getBytes().length 而不能直接使用 response.length()
// 这两者的区别就在于前者是获取到字符串转换成字节数组后的长度,即字节个数,后者是获取到字符串的长度,即字符个数
// 在有些情况下,一个字符的长度并不是一个字节,例如一个汉字是两个字节
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, packet.getSocketAddress());
// 6. 将构造好的响应数据传回给客户端
socket.send(responsePacket);
}
}
private String process(String request) {
// 因为是回显服务,所以直接返回请求数据
return request;
}
public static void main(String[] args) throws IOException {
// 处理异常 并指定好服务器端口号
UDPEchoServer echoServer = new UDPEchoServer(9090);
// 调用start 方法启动服务器
echoServer.start();
}
}
客户端代码
public class UDPEchoClient {
private DatagramSocket socket = null;
private String ip;
private int port;
public UDPEchoClient(String ip, int port) throws SocketException {
// 无参的构造方法表示使用系统分配的端口
socket = new DatagramSocket();
// 通过参数的 ip 和端口指定需要连接的服务器 ip 和端口
this.ip = ip;
this.port = port;
}
public void start() throws IOException {
// 通过输入读取客户端请求
Scanner scanner = new Scanner(System.in);
System.out.println("客户端启动");
while(true) {
String request = scanner.nextLine();
// 将输入的请求构造成数据报, 将在构造方法中设置好的 ip 和端口传入,表明数据的去向
DatagramPacket packet = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(ip), port);
// 将数据发送给服务器
socket.send(packet);
// 接收服务器返回的响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
// 将服务器返回的数据构造成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
// 打印出构造好的响应字符串
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
// 由于服务器在本机,所以使用本机 ip “127.0.0.1”, 端口号为服务器代码中指定的端口号
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 9090);
udpEchoClient.start();
}
}
运行效果
TCP回显服务
我们同样先看一下 TCP 的 socket API 的两个主要的类。
ServerSocket;
Socket;
TCP 协议和 UDP 协议存在很大的差别,因此提供 socke API 也有很大的差别。首先,TCP 是面相字节流传输数据,所以不需要将数据包装成数据报,其次,TCP 传输数据是有连接的,因此,不能像 UDP 那样直接传输或者接收数据,需要先让客户端和服务器建立连接。这里的 ServerSocket 是专门提供给服务器使用的类,它的作用很简单,就是建立客户端和服务器之间的连接,后续的操作都通过 Socket 完成。
服务器代码
public class TCPEchoServer {
private ServerSocket serverSocket = null;
private int port;
// 指定服务器端口
public TCPEchoServer(int port) throws IOException {
this.port = port;
serverSocket = new ServerSocket(port);
}
public void start () throws IOException {
System.out.println("服务器启动");
while(true) {
// 循环建立连接,此处需要使用多线程处理客户端请求
// 如果单线程处理,那在一个客户端与服务器建立连接后,直到这个连接断开之前,服务器都不能与别的客户端建立连接
// 使用多线程就可以让新的线程去处理请求,而服务器直接进入下一次循环,等待新的客户端建立连接
// 如果没有客户端进行连接,accept() 就会进入阻塞等待状态。
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(()-> {
// 处理请求
processConnection(clientSocket);
});
// 启动线程
t.start();
}
}
private void processConnection(Socket clientSocket) {
System.out.println("客户端建立连接");
// 由于 TCP 是面相字节流,所以针对 TCP socket 的读写可以直接使用针对文件的字节流读写操作。
// 这里不再是自己 new 对象,而是直接获取 socket 持有的对象
try (InputStream inputStream = clientSocket.getInputStream()) {
// 通过字节流对象的 read 方法读取到的是字节数组
// 而 Scanner 可以直接将字节流对象中的数据读取成字符串,这样更方便
Scanner scanner = new Scanner(inputStream);
try (OutputStream outputStream = clientSocket.getOutputStream()) {
// 循环处理请求
while(true) {
if (!scanner.hasNext()) {
// 如果 scanner 没有继续读了,就断开连接
System.out.println("服务器断开连接");
break;
}
// 获取请求字符串
String request = scanner.next();
// 打印请求字符串 做简单验证
System.out.println(request);
// 处理请求,获取响应
String response = process(request);
// 使用 PrintWriter 包裹 OutputStream
// 这个操作类似上面的 Scanner,可以让我们的写操作更方便
PrintWriter printWriter = new PrintWriter(outputStream);
// 将响应通过上面包裹的 outputStream 写回 socket 文件
printWriter.println(response);
// 打印字符串进行简单验证
System.out.println(response);
// 刷新一下缓存区,让客户端能更快的看到响应数据
printWriter.flush();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9090);
tcpEchoServer.start();
}
}
客户端代码
public class TCPEchoClient {
private Socket socket = null;
public TCPEchoClient(String ip, int port) throws IOException {
// 这里指定的 IP 和端口号并不是绑定给客户端,而是表示客户端需要连接的服务器的 IP 和端口
socket = new Socket(ip, port);
}
public void start() {
System.out.println("与服务器连接成功");
// 接下来的读写操作与服务器类似
try(InputStream inputStream = socket.getInputStream()) {
// 读取客户输入的请求
Scanner scanner = new Scanner(System.in);
try(OutputStream outputStream = socket.getOutputStream()) {
while(true) {
// 构造请求
String request = scanner.next();
// 把请求写入 socket 并传输给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 刷新缓冲区,让服务器第一时间看到数据
printWriter.flush();
// 从服务器读取响应数据
Scanner responseScanner = new Scanner(inputStream);
// 构造成响应字符串
String response = responseScanner.next();
// 打印响应
System.out.println(response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9090);
tcpEchoClient.start();
}
}
运行效果
以上就是对网络编程套接字,即socket API 和 socket 的一些简单介绍,如有问题,欢迎指正补充。