Java 中提供了专门的网络编程程序包 java.net,提供了两种通信协议:UDP(数据报协议)和 TCP(传输控制协议),本文对两种通信协议的开发进行详细介绍。
1
UDP 介绍
UDP:User Datagram Protocol,是一种无连接的传输层协议,是不可靠的消息传输服务。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。
UDP 的特点是非面向连接,传输不可靠,数据可能丢失。一般用于各种聊天工具、音频或者多媒体应用,对数据传输的完整性要求不是特别高的场景。
在 UDP 开发中使用 DatagramPacket 类包装一条要发送的消息,然后用 DatagramSocket 类完成消息的发送。
下面我们来通过一个的示例来了解 UDP 编程,编写一个客户端程序,在客户端里指定要接收数据(服务端)的端口以及要发送的数据,打包数据后进行发送;编写一个服务端程序,在服务端里接收客户端发来的数据并输出。
// 客户端发送消息 @Test public void testClient() throws IOException { // 1、创建客户端指定端口为6666 DatagramSocket client = new DatagramSocket(6666); // 2、准备数据 String msg = "测试UDP!"; byte[] data = msg.getBytes(); // 3、打包数据,InetSocketAddress端口为服务端的端口 DatagramPacket packet = new DatagramPacket(data, data.length, new InetSocketAddress("localhost", 8888)); // 4、发送数据 client.send(packet); // 5、释放资源 client.close(); } // 服务端接收消息 @Test public void testServer() throws IOException { // 1、创建服务端指定端口为8888 DatagramSocket server = new DatagramSocket(8888); // 2、准备接受输入的缓存字节数组 byte[] buf = new byte[1024]; // 3、封装成DatagramPacket包 DatagramPacket packet = new DatagramPacket(buf, buf.length); // 4、接受数据 server.receive(packet); // 5、分析数据 byte[] data = packet.getData(); System.out.println(new String(data, 0, packet.getLength())); // 6、释放资源 server.close(); }
首先运行服务端程序,然后运行客户端程序,观察服务端程序的控制台的运行结果:
测试UDP!
下面我们结合 IO 流,来演示一个发送基本数据类型的示例,客户端向服务端发送一个用户的名称和年龄,假设用户名为 JPM,年龄为 18 岁。
// 客户端发送数据 @Test public void client() throws IOException { // 1、创建客户端指定端口为6666 DatagramSocket client = new DatagramSocket(6666); // 2、准备数据 String name = "JPM"; int age = 18; byte[] data = buildData(name, age); // 3、打包数据,InetSocketAddress端口为服务端的端口 DatagramPacket packet = new DatagramPacket(data, data.length, new InetSocketAddress("localhost", 8888)); // 4、发送数据 client.send(packet); // 5、释放资源 client.close(); } private byte[] buildData(String name, int age) throws IOException { byte[] data = null; ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeUTF(name); dos.writeInt(age); dos.flush(); data = bos.toByteArray(); // 获取数据 dos.close(); bos.close(); return data; } // 服务端接收数据 @Test public void server() throws IOException { // 1、创建服务端指定端口为8888 DatagramSocket server = new DatagramSocket(8888); // 2、准备接受输入的缓存字节数组 byte[] buf = new byte[1024]; // 3、封装成DatagramPacket包 DatagramPacket packet = new DatagramPacket(buf, buf.length); // 4、接受数据 server.receive(packet); // 5、分析数据 String data = getData(packet.getData()); System.out.println(data); // 6、释放资源 server.close(); } private String getData(byte[] data) throws IOException { DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data)); String name = dis.readUTF(); int age = dis.readInt(); dis.close(); return name + "的年龄是:" + age; }
首先运行服务端程序,然后运行客户端程序,观察服务端程序的控制台的运行结果:
JPM的年龄是:18
2
TCP 介绍
TCP,Transmission Control Protocol,是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 的特点是面向连接的、点到点的、高可靠的,需要通过三次握手建立连接。
在 Java 中使用 Socket(套接字)完成 TCP 程序的开发。服务器端使用 ServerSocket 类来接受客户端的连接,每一个客户端都使用一个 Socket 对象表示。
服务器端要使用 accept() 方法等待客户端的连接,此方法执行后服务器端进入阻塞状态,直到客户端连接之后程序才可以继续执行。accept() 方法返回的是一个 Socket 类型的对象,每一个 Socket 对象都表示一个客户端对象。
在客户端,可以通过 Socket 类的 getInputStream() 方法获取服务器的输出信息,在服务器端可以通过 Socket 类的 getOutputStream() 方法获取客户端的输出信息,因此说网络程序员要使用 Java IO 输入、输出流来完成信息的传递。
下面演示一个 TCP 程序的示例,服务器接收到客户端的连接后返回给客户端“Hello,欢迎使用!”,客户端接收服务端的数据并输出到控制台。
// 服务器端程序 @Test public void testServer() throws IOException { // 1、创建服务器指定8888端口 ServerSocket server = new ServerSocket(8888); // 2、接收客户端连接,阻塞式 Socket socket = server.accept(); System.out.println("建立客户端连接"); // 3、发送数据 String msg = "Hello,欢迎使用!"; BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write(msg); // 必须调用此方法,否则客户端readLine()会执行会发生Connection reset异常 bw.newLine(); bw.flush(); server.close(); } // 客户端程序 @Test public void testClient() throws IOException { // 1、创建客户端连接,指定服务器的域名和端口 Socket client = new Socket("localhost", 8888); // 2、接收数据 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); String msg = br.readLine(); // 阻塞式方法 System.out.println(msg); client.close(); }
首先运行服务端程序,然后运行客户端程序,观察控制台的运行结果:
服务端控制台输出: 建立客户端连接 客户端控制台输出: Hello,欢迎使用!
注意:如果服务端已经开启一个 8888 端口的服务,那么再次开启 8888 端口服务端的程序会发生绑定端口异常,如:java.net.BindException: Address already in use: JVM_Bind
以上的程序一个服务端只能接收一个客户端,并只能处理一次客户端请求,如果想实现一个服务端可以接收多个客户端的连接,那么需要在服务端程序上加入一个 while 无限循环来完成。
3
TCP 通信的经典案例
下面的代码实现网络编程通信的一个经典案例,就是客户端输入消息,服务器接收到客户端的消息后,在客户端发来的消息内容前面加上“echo:”并返回给客户端,直到客户端输入“exit”时退出连接。
// 服务器端程序 @Test public void testMultiServer() throws IOException { // 创建服务器指定8888端口 ServerSocket server = new ServerSocket(8888); Socket client = null; PrintStream out = null; BufferedReader buf = null; boolean sts = true;// 默认无限循环 // while循环接收客户端连接 while (sts) { System.out.println("======服务器已经运行,等待客户端的链接:======"); client = server.accept(); System.out.println("建立客户端连接"); // 得到客户端的输入信息 buf = new BufferedReader(new InputStreamReader(client.getInputStream())); // 实例化客户端的输出流,用于输出消息 out = new PrintStream(client.getOutputStream()); boolean flag = true; while (flag) { String str = buf.readLine();// 不断接收输入的消息 if ("exit".equals(str)) { // 客户端结束输入 flag = false; } else { out.println("echo:" + str); } } out.close(); client.close(); } server.close(); } // 客户端程序 @Test public void testClient2() throws IOException { // 创建客户端连接,指定服务器的域名和端口 Socket client = new Socket("localhost", 8888); // 接收服务器端的输入 BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream())); // 从键盘接收输入 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // 向服务器端输出消息 PrintStream out = new PrintStream(client.getOutputStream()); boolean flag = true; while (flag) { System.out.print("请输入消息:"); String msg = reader.readLine();// 从键盘接收输入的数据 out.println(msg); // 输出到服务器端 if ("exit".equals(msg)) { flag = false; } else { System.out.println(buf.readLine()); // 输出服务器返回的消息 } } client.close(); reader.close(); buf.close(); }
首先运行服务端程序,然后运行客户端程序,观察控制台的运行结果:
服务端控制台输出: ======服务器已经运行,等待客户端的链接:====== 建立客户端连接 ======服务器已经运行,等待客户端的链接:====== 建立客户端连接 ======服务器已经运行,等待客户端的链接:====== 客户端1控制台: 请输入消息:你好,追梦Java echo:你好,追梦Java 请输入消息:hello,java echo:hello,java 请输入消息:exit 客户端2控制台: 请输入消息:客户端2 echo:客户端2 请输入消息:hello,客户端2 echo:hello,客户端2 请输入消息:exit
以上的程序结果来看,所有的客户端输入的内容,都会在服务器端加上“echo:”后回显给客户端,同时当一个客户端退出时,服务器端没有退出,而是会等待下一个客户端进行连接,服务器端可以重复执行。
但是在以上的程序中存在一个服务器端单线程处理的问题,也就是说以上的程序服务器端每次只能有一个客户端连接,其他的客户端只有等前面的客户端退出后,自己才能执行。
可以通过上面的程序,首先启动服务端,再启动一个客户端,然后再启动一个客户端的过程来感受。
4
多线程机制下的 TCP 编程
上面程序中存在服务器单线程服务的问题,由于 accept() 方法的阻塞机制,因此当客户端没有退出时,一个服务器只能处理已经连接上的客户端程序,在这个客户端没有退出前,其他客户端是无法连接的。
为了保证服务器可以同时连接多个客户端,可以在服务器端的程序里加上多线程机制,也就是说,每个客户端连接之后都启动一个线程,这样一个服务器就可以同时支持过个客户端连接了。
在服务器端加上多线程机制,就是需要在每个客户端连接之后启动一个新的线程。
因此,我们需要建立一个专门用于处理多线程操作的 EchoThread 类,实现 Runnable 接口。
public class EchoThread implements Runnable { private Socket client = null; public EchoThread(Socket client) { this.client = client; } @Override public void run() { PrintStream out = null; BufferedReader buf = null; try { // 得到客户端的输入信息 buf = new BufferedReader(new InputStreamReader(client.getInputStream())); // 实例化客户端的输出流 out = new PrintStream(client.getOutputStream()); boolean flag = true; // 表示一个客户端是否结束 while (flag) { String str = buf.readLine(); if ("exit".equals(str)) { flag = false; } else { out.println("echo:" + str); } } out.close(); buf.close(); client.close(); } catch (Exception e) { e.printStackTrace(); } } }
下面在服务器程序 EchoThreadServer 类加上多线程的调用,使用 EchoThread 类:
public class EchoThreadServer { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8888); Socket client = null; boolean flag = true; while (flag) { System.out.println("======服务器已经运行,等待客户端的链接:======"); client = server.accept(); // 启动一个EchoThread线程来处理一个独立的客户单 new Thread(new EchoThread(client)).start(); } server.close(); } }
客户端程序不变,首先运行服务端程序 EchoThreadServer,然后分别运行客户端程序两次,观察控制台的运行结果:
服务端控制台输出: ======服务器已经运行,等待客户端的链接:====== 建立客户端连接 建立客户端连接 客户端1控制台: 请输入消息:我是客户端1 echo:我是客户端1 请输入消息:hello1 echo:hello1 请输入消息:exit 客户端2控制台: 请输入消息:我是客户端2 echo:我是客户端2 请输入消息:hello2 echo:hello2 请输入消息:exit
从上面的例子可以看出,服务端支持多个客户端的连接,并且每个客户端都可以独立与服务端进行交互,互不影响。
通过上面的介绍,对 Java 的网络编程有了进一步的认识,后面会基于以上的内容结合 Java IO 流来实现一个聊天室的功能,敬请期待......