> 文章列表 > JavaEE——网络编程套接字Socket

JavaEE——网络编程套接字Socket

JavaEE——网络编程套接字Socket

操作系统为我们实现了传输层及以下的协议,程序猿要做的主要是实现应用层方面的协议,也就是网络编程。

目录

网络编程

Socket套接字

UDP数据报套接字

TCP流套接字


网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。即使是同一个主机,只要进程不同,通过网络传输数据也属于网络编程的范围。

学习网络编程,首先需要了解: 

  • 接收端与发送端:数据的接收方进程,即此次通信中的目的IP,称为接收端;数据的发送方进程,即此次通信中的源IP,称为发送端。发送端与接收端都可以简称为收发端。
  • 客户端服务器:网络传输中提供服务的一端称为服务器。获取服务的一端称为客户端。
  • 请求和响应:客户端发送请求,服务器响应数据并反馈。

假如我去餐馆吃饭,那么我就是客户端,餐馆就是服务器。我要了一份炒饭,这就是请求,餐馆把炒饭做出来并放到我旁边,就是响应。

Socket套接字

Socket套接字,是基于TCP/IP协议的网络通信的基本操作单元,用于网络通信。基于Socket套接字的开发就是网络编程。

Socket套接字针对传输层协议分为三类:

流套接字:使用传输层TCP协议。

数据报套接字:使用传输层UDP协议。

原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据。

TCP协议特点

  • 有连接:基于TCP协议实现的程序,必须先连接才能传输(就像打电话,打通了以后才能说话)。
  • 可靠性传输:TCP协议尽可能地保证数据传输,关心对方是否收到。
  • 面向字节流:以一个字节一个字节地传输。
  • 全双工:双向通信,对应半双工(单向传输)。
  • 传输大小不限:每次传输的空间大小不受限制。

UDP协议特点

  • 无连接:不需要连接就可以通信(相当于发短信)。
  • 不可靠传输:发送数据后就不管了,不关心对方是否收到。
  • 面向数据报:每次传输只能以数据报为单位,不能一次传输半个数据报。(假如一个数据报包含了100个字节,那么这100个字节只能一起发送,不能分开发)
  • 全双工:双向通信。
  • 大小受限:每次传输最多传输64kb。

UDP数据报套接字

UDP数据报套接字需要用到DatagramSocket的相关API。

DatagramSocket的核心方法是receive()和send(),同时,需要一个DatagramPacket类,也就是数据报来接收请求或者响应,UDP服务器实现流程是这样的:

  1. 初始化一个服务器对象,这个对象的端口应该手动指定(端口号用来识别不同的服务器,如果自动分配客户端就难以找到相应的服务器了)。
  2. 实现其UDP的特点:面向数据报,也就是要创建一个DatagramPacket类来接收请求(可以把它当作我们去食堂打饭用的盘子),接着在receive方法种传入该数据报实例对象,receive方法会接收datagramPacket中的数据,如果没有数据就阻塞等待,直到客户端输入请求(相当于我们把盘子递给打饭阿姨,阿姨打好饭以后才能把盘子递给我们,否则只能等待)。
  3. 把接收到的请求转换为字符串并计算响应:提取datagramSocket中的所有信息转换成字符串,再调用响应函数计算出响应。
  4. 创建一个新的数据报,并把响应内容传入,最后把数据报通过send函数传给客户端。

当然,只要客户端发起请求,服务器都要响应,因此服务器需要循环的处理响应。

public class UdpEchoServer {private DatagramSocket socket;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true) {//创建接收信息的空间DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);//接收信息socket.receive(requestPacket);//处理信息String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), StandardCharsets.UTF_8);String response = process(request);//这里的响应由开发者实现//发送信息到客户端DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());//最后一个参数是客户端的地址(请求与响应需要对应)socket.send(responsePacket);//打印信息System.out.printf("[%s: %d] req: %s, res: %s\\n",requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);}}public String process(String request) {//这里计算响应结果}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}

注意这里的字符串应该先转换为byte数组在计算长度,假如字符串中包含汉字等字符,那么得出的长度就不对了。 

与之相对的客户端类似:

客户端不需要直到自己的端口号,因此不必手动指定(客户端的数量很多,如果端口号重复则这些客户端都无法使用),交给系统自动分配即可。但是客户端需要指定服务器的IP和端口号。

客户端流程:输入请求->用数据报接收->send到服务器,使用数据报接收响应结果->使用响应结果

public class UdpClient {private final DatagramSocket socket;private final String serverIP;private final int serverPort;public UdpClient(String ip, int port) throws SocketException {socket = new DatagramSocket();serverIP = ip;serverPort = port;}public void start() throws IOException {Scanner scanner = new Scanner(System.in);while (true) {System.out.print("-> ");//输入请求String request = scanner.next();DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIP), serverPort);//最后两个参数用于指定服务器的IP和端口号//发送请求到服务器socket.send(requestPacket);//接收服务器的处理结果DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);socket.receive(responsePacket);//打印信息String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), StandardCharsets.UTF_8);System.out.printf("req:  %s, res: %s\\n", request, response);}}public static void main(String[] args) throws IOException {UdpClient client = new UdpClient("127.0.0.1", 9090);client.start();}
}

同样需要注意输入的请求先转换为byte数组再计算长度。

TCP流套接字

TCP流套接字的实现与UDP大不相同,其实从二者的特点就可以看出来。

TCP流套接字不再使用DatagramSocket类,而是使用ServerSocket和Socket类,其中ServerSocket只做一件事,那就是与客户端建立连接,剩下的交给Socket类处理。

TCP服务器实现流程:

  1. 构造服务器对象(指定端口号)
  2. 建立连接:建立连接通过ServerSocket类的accept方法实现,返回Socket,因为服务器需要处理多个客户端的请求,因此需要循环的调用accept方法以便于客户端可以及时连接到服务器。
  3. 读取请求并处理:TCP的特点是面向字节流,因此需要用到io包中的InputStream和OutputStream,通过Scanner读取input Stream中的数据,读取到的数据(也就是客户端发起的请求)交给响应函数处理,当然也需要循环的处理请求。
  4. 返回响应到客户端:利用PrintWriter类的println方法写入到outputStream中,通过flush函数刷新缓冲区,把内容提交给客户端(如果对PrintWriter类的方法不太熟悉的可以看一下:文件操作与IO操作)。

由于客户端与服务器建立了连接,当客户端断开连接时,需要及时的调用close方法关闭连接(虽然不关闭也问题不大,但最好还是关闭,毕竟占用了资源)。

public class TcpEchoServer {private final ServerSocket serverSocket;public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true) {//等待客户端连接System.out.println("等待客户端连接…");Socket clientSocket = serverSocket.accept();processConnect(clientSocket);}}private void processConnect(Socket clientSocket) {System.out.printf("[%s: %d] 客户端连接成功!\\n",clientSocket.getInetAddress().toString(), clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream()) {try (OutputStream outputStream = clientSocket.getOutputStream()) {Scanner scanner = new Scanner(inputStream);//循环处理请求while (true) {if (!scanner.hasNextLine()) {//读取完毕System.out.printf("[%s: %d] 客户端断开连接!\\n",clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}String request = scanner.nextLine();String response = process(request);PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(response);printWriter.flush();//刷新缓冲区,确保第一时间获取请求System.out.printf("[%s: %d] req: %s, res: %s\\n",clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);}}} catch (IOException e) {e.printStackTrace();} finally {try {clientSocket.close();//关闭资源} catch (IOException e) {e.printStackTrace();}}}public String process(String request) {//这里处理请求返回响应}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}

对应的客户端实现流程:创建客户端实例对象(使用Socket类),构造时需要传入参数:IP和端口号(这里的IP和端口号为连接的服务器IP和端口号)->输入请求->请求通过PrintWriter写入outputStream中->刷新缓冲区,把请求提交给服务器->接收服务器返回的响应:

public class TcpClient {private Socket socket;public TcpClient(String ServerIP, int serverPort) throws IOException {socket = new Socket(ServerIP, serverPort);//相当于打电话的拨号}public void start() {try (InputStream inputStream = socket.getInputStream()) {try (OutputStream outputStream = socket.getOutputStream()) {System.out.println("连接成功!");Scanner scanner = new Scanner(System.in);while (true) {//输入请求System.out.print("-> ");String request = scanner.nextLine();//发送请求到服务器PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(request);printWriter.flush();//接收服务器响应结果Scanner resScanner = new Scanner(inputStream);String response = resScanner.nextLine();//输入结果System.out.printf("req: %s res : %s\\n", request, response);}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpClient client = new TcpClient("127.0.0.1", 9090);client.start();}
}