3.5 一对多应用设计
点对点聊天的缺点:因为客户端和服务端之间的连接是在主线程中,主线程的作用是启动接受和发送消息的线程,一旦主线程任务完成,accpet方法也结束了,那么服务端就无法再接受其他客户端的连接了。此时,只要将accpet单独拿出来,使服务端监听到每个与之连接的客户端,并返回对应的Socket对象即可。但是这样,客户端与客户端之间是隔离的,不能相互通信。
3.5.1 一对多应答型服务端
应答型客户端指的是:一个服务端和多个客户端连接,客户端向服务端发送数据时服务端再将数据返回给客户端。
//主线程 public class EchoServer { public static void main(String[] args) { try(ServerSocket serverSocket = new ServerSocket(8888)) { while (true){ Socket socket = serverSocket.accept();//不能死 new Msg(socket).start(); } }catch (Exception e){ e.printStackTrace(); } } }
3.5.2 一对多聊天应用实现
聊天服务器就是多个客户端和服务端连接,当一个客户端向服务端发送数据的时候,服务端会将这些数据发送给其他和服务端相连的客户端。
服务器的线程设计
实现思路,服务端的接受线程负责接受客户端的消息,接受到消息后将消息写入到公共数据区,与此同时唤醒所有客户端的发送线程将缓冲区中的数据发送给客户端。然后发送线程又处于阻塞状态
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; //接受客户端消息的线程 class ChatReceive extends Thread{ private Socket socket;//拿到客户端对应的socket对象 public ChatReceive(Socket socket){ this.socket = socket; } public void receiveMsg(){ try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { while (true){ String s = bufferedReader.readLine();//读消息不受锁影响 synchronized ("aaa"){ //将读取的数据写入公共数据区 ChatRoomServer.buf = "["+socket.getInetAddress()+"]"+s; //唤醒所有发送消息的线程 "aaa".notifyAll(); } } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { receiveMsg(); } } //向客户端发送消息的线程 class ChatSend extends Thread{ private Socket socket;//拿到客户端对应的socket对象 public ChatSend(Socket socket){ this.socket = socket; } public void sendMsg(){ try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream());) { while (true){ //加锁,让线程实现同步(互斥) synchronized ("aaa"){ //让发送消息的线程处于等待状态,只有服务端接受到数据后才能将数据发送出去。 "aaa".wait(); //表示拥有“aaa”对象锁的线程处于阻塞状态。 printWriter.println(ChatRoomServer.buf); printWriter.flush(); } } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { sendMsg(); } } //主线程,专门负责客户端和服务端之间的连接 public class ChatRoomServer { public static String buf; //公共数据区 public static void main(String[] args) { System.out.println("服务端启动"); try(ServerSocket serverSocket = new ServerSocket(8888)) { while (true){ Socket socket = serverSocket.accept();//不能死 System.out.println("连接到:"+socket.getInetAddress()); new ChatReceive(socket).start(); //启动线程 new ChatSend(socket).start(); } }catch (Exception e){ e.printStackTrace(); } } }
package cn.it.bz.Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; //接受消息 class ClientChatReceive extends Thread{ private Socket socket; public ClientChatReceive(Socket socket) { this.socket = socket; } public void accept(){ try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()))) { while (true){ System.out.println( bufferedReader.readLine()); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { accept(); } } //发送消息 class ClientChatSend extends Thread{ private Socket socket; public ClientChatSend(Socket socket) { this.socket = socket; } public void send(){ try(PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) { System.out.println("请输入消息:"); Scanner scanner = new Scanner(System.in); while (true){ String s = scanner.nextLine(); printWriter.println(s); printWriter.flush(); } }catch (Exception e){ e.printStackTrace(); } } @Override public void run() { send(); } } //聊天室客户端 public class ChatRoomClient { public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1",8888); new ClientChatSend(socket).start(); new ClientChatReceive(socket).start(); }catch (Exception e){ e.printStackTrace(); } } }
四、UDP通信
4.1 UDP实现原理
UDP协议与之前讲到的TCP协议不同,是面向无连接的,双方不需要建立连接便可通信。UDP通信所发送的数据需要进行封包操作(使用DatagramPacket类),然后才能接收或发送(使用DatagramSocket类)。虽然不可靠,但是效率高,视频会议网络聊天一般使用UDP协议。
DatagramPacket:数据容器(封包)的作用
此类表示数据报包。 数据报包用来实现封包的功能。
DatagramSocket:用于发送或接收数据报包
当服务器要向客户端发送数据时,需要在服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。
DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:
- DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。
- DatagramSocket(int port) :创建数据报套接字并将其绑定到本地主机上的指定端口。
常用方法
方法名 | 使用说明 |
send(DatagramPacket p) | 从此套接字发送数据报包 |
receive(DatagramPacket p) | 从此套接字接收数据报包 |
close() | 关闭此数据报套接字 |
4.2 UDP通信编程基本步骤
- 创建客户端的DatagramSocket,创建时,定义客户端的监听端口。
- 创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口。
- 在服务器端定义DatagramPacket对象,封装待发送的数据包。
- 客户端将数据报包发送出去。
- 服务器端接收数据报包。
4.3 实现UDP通信
4.3.1 创建服务端
package cn.it.bz.Socket; import java.net.DatagramPacket; import java.net.DatagramSocket; public class UDPServer { public static void main(String[] args) { //创建基于UDP协议的DatagramSocket对象。 //表示服务端要监听该电脑的8888端口。 try(DatagramSocket datagramSocket = new DatagramSocket(8888);) { //创建数据缓冲区 byte[] bytes = new byte[1024]; //创建数据包对象(就相当于是放东西的袋子) DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length); //等待接受客户端发送数据(服务端拿着袋子准备装数据) datagramSocket.receive(datagramPacket); //取出袋子中的数据 byte[] data = datagramPacket.getData(); //将字节数据转换成字符串类型【offset表示从数组哪个位置开始,datagramPacket.getLength()表示袋子中数据包的长度】 String s = new String(data,0,datagramPacket.getLength()); System.out.println("客户端发送的数据是:"+s); }catch (Exception e){ e.printStackTrace(); } } }
4.3.2 创建客户端
package cn.it.bz.Socket; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; public class UDPClient { public static void main(String[] args) { //该端口为UDP协议发送数据指定端口。 // 如果服务端和客户端在一个设备上,那么客户端指定的端口和服务端监听的端口不能相同,否则会发生端口抢占。 try( DatagramSocket datagramSocket = new DatagramSocket(9999);) { //数据需要转换为字节数组类型 byte[] bytes = "Java".getBytes(); //创建数据报包对象.后两个参数指定数据发送到哪个服务端以及服务端接受消息的端口 DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888)); //向服务端发送数据 datagramSocket.send(datagramPacket); }catch (Exception e){ e.printStackTrace(); } } }
4.4 基于UDP协议传输基本数据类型
4.4.1 创建服务端
package cn.it.bz.Socket; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; public class BasicTypeUDPServer { public static void main(String[] args) { try(DatagramSocket datagramSocket = new DatagramSocket(8888);) { //字节数组 byte[] bytes = new byte[1024]; //存放数据的袋子 DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length); //等待接受客户端数据 datagramSocket.receive(datagramPacket); //从袋子取数据 byte[] data = datagramPacket.getData(); //通过DataInputStream对象读取基本数据类型 //ByteArrayInputStream(bytes)可以直接从内存读取字节 try(DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(bytes))) { System.out.println("客户端发送的数据:"+dataInputStream.readLong()); } }catch (Exception e){ e.printStackTrace(); } } }
4.4.2 创建客户端
package cn.it.bz.Socket; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; public class BasicTypeUDPClient { public static void main(String[] args) { long data = 1000000L; try(DatagramSocket datagramSocket = new DatagramSocket(9999); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream); ) { //将基本数据类型转换为数组 dataOutputStream.writeLong(data); //将基本数据类型转换为数组 byte[] bytes = byteArrayOutputStream.toByteArray(); //数据包装 DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888)); //数据发送 datagramSocket.send(datagramPacket); }catch (Exception e){ e.printStackTrace(); } } }
4.5 基于UDP协议传递自定义对象
4.5.1 创建对象
package cn.it.bz.Socket; import java.io.Serializable; //使用流操作对象必须要实现序列化接口 public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
4.5.2 创建服务端
package cn.it.bz.Socket; import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; public class ObjectTypeServer { public static void main(String[] args) { try(DatagramSocket datagramSocket = new DatagramSocket(8888)) { byte[] bytes = new byte[1024]; DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length); datagramSocket.receive(datagramPacket); //接受数据 byte[] data = datagramPacket.getData(); //将对象转换为字节数组 try(ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data))) { Person person = (Person) objectInputStream.readObject(); System.out.println(person); }catch (Exception e){ e.printStackTrace(); } }catch (Exception e){ e.printStackTrace(); } } }
4.5.3 创建客户端
package cn.it.bz.Socket; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; public class ObjectTypeClient { public static void main(String[] args) { try(DatagramSocket datagramSocket = new DatagramSocket(9999); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//ByteArrayOutputStream将内存的数据读取到字节数组中,将对象转换为字节数组 ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { //创建自定义对象 Person person = new Person("zhangsan",13); objectOutputStream.writeObject(person); byte[] bytes = byteArrayOutputStream.toByteArray(); //数据包装 DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",8888)); //发送数据 datagramSocket.send(datagramPacket); }catch (Exception e){ e.printStackTrace(); } } }