文章目录
1.题目描述
实现一个远程调用词典翻译的例程,题目如下:
2. 在服务器端实现词典
2.1 实现词典接口
public interface Translator{
public String translate(String str);
}
2.2 实现词典类
public class Translators implements Translator{
private HashMap<String, String> dic;// 词典
public Translators(){
initDic();
}
// 初始化词典,我这里随便写了个词典,只是为了测试程序
private void initDic() {
dic = new HashMap<>();
dic.put("中国", "China");
dic.put("苹果", "apple");
dic.put("拜拜", "bye");
}
// 根据输入在词典中查找翻译结果,如果没有找到返回null
public String translate(String read) {
String res = dic.getOrDefault(read, null);
// 如果key中找不到,则在value中查找
if(res==null){
for(String key: dic.keySet()){
if(dic.get(key).equals(read)){
res = key;
}
}
}
return res;
}
}
3. UDP协议(DatagramSocket)实现远程词典调用
DatagramSocket使用UDP协议发送数据。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。
3.1 发送端
首先定义一个UdpClient类,其中包含send方法和receive方法。需要注意的是,DatagramPacket仅可以按字节传输数据,所以要传输的数据必须转成byte[]类型。
public class UdpClient {
public void send(DatagramSocket ds,String line)throws IOException{
byte[] bys = line.getBytes();
DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("localhost"),10086);
ds.send(dp);
}
public String receive(DatagramSocket ds) throws IOException {
byte[] bys1 = new byte[1024];
DatagramPacket dp_1 = new DatagramPacket(bys1, bys1.length);
ds.receive(dp_1);
System.out.print(dp_1.getAddress()+":"+dp_1.getPort()+"发送信息:");
return new String(dp_1.getData(), 0, dp_1.getLength());
}
}
发送端的主函数中,在一个死循环中不断地发送并接收数据,当连续两次从系统输入“bye”时,发送端跳出循环,并终止程序。
public static void main(String[] args) throws IOException {
UdpClient udpclinet=new UdpClient();
DatagramSocket ds = new DatagramSocke ();//随机指派端口
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line=br.readLine();
int k=0;
while(true) {
udpclinet.send(ds,line);//发送数据
String data=udpclinet.receive(ds);//接收数据
System.out.println(data);//打印接收到的数据
// 判断是否中止连接
if(line.equals("bye")){
k=1;
System.out.println("如果你想退出,请再次输入“bye”,否则,该连接不会终止!");
}
line = br.readLine();
if(line.equals("bye")&&k==1) break;
else k=0;
}
}
3.2 接收端
首先定义一个UdpServer类,同样包含send方法和receive方法,除此之外还有addr和port实例域。UdpServer类基本上与UdpClient一致,这是因为对于UDP协议而言,并没有严格的发送端与接收端的区分,发送端可以作为接收端,接收端也可以作为发送端。
其中addr和port分别用来存储接收到的数据报的ip地址和端口,当用到send方法时会调用addr和port这两个实例域,以保证翻译结果发送到正确的地址。
public class UdpServer {
private InetAddress addr;
private int port;
public void send(DatagramSocket ds,String line)throws IOException{
byte[] bys = line.getBytes();
DatagramPacket dp = new DatagramPacket(bys, bys.length, addr,port);
ds.send(dp);
}
public String receive(DatagramSocket ds) throws IOException {
byte[] bys1 = new byte[1024];
DatagramPacket dp_1 = new DatagramPacket(bys1, bys1.length);
ds.receive(dp_1);
this.addr=dp_1.getAddress();
this.port=dp_1.getPort();
System.out.print(addr+":"+port+"发送信息:");
return new String(dp_1.getData(), 0, dp_1.getLength());
}
}
接收端的主函数中,在一个死循环中不断地接收并发送数据,并且该死循环永不退出。
public static void main(String[] args) throws IOException {
UdpServer udpserver=new UdpServer();
DatagramSocket ds = new DatagramSocket(10086);//本地端口
Translators t=new Translators();//翻译器
//接收并解析数据包
String data_s = udpserver.receive(ds);
while (true) {
System.out.println(data_s);
String trans=data_s+"的翻译结果: "+t.translate(data_s);
udpserver.send(ds,trans);
data_s = udpserver.receive(ds);
}
}
3.3远程词典调用
首先启动接收端,然后启动发送端(实际上由于UDP协议只发送数据,不需要关心接收端是否收到,所以接收端和发送端的启动顺序并没有严格要求)。
发送端窗口:
apple
/127.0.0.1:10086发送信息:apple的翻译结果: 苹果
中国
/127.0.0.1:10086发送信息:中国的翻译结果: China
拜拜
/127.0.0.1:10086发送信息:拜拜的翻译结果: bye
demo
/127.0.0.1:10086发送信息:demo的翻译结果: null
bye
/127.0.0.1:10086发送信息:bye的翻译结果: 拜拜
如果你想退出,请再次输入“bye”,否则,该连接不会终止!
bye
Process finished with exit code 0
接收端窗口:
/127.0.0.1:64650发送信息:apple
/127.0.0.1:64650发送信息:中国
/127.0.0.1:64650发送信息:拜拜
/127.0.0.1:64650发送信息:demo
/127.0.0.1:64650发送信息:bye
4. TCP协议(Socket)实现远程词典调用
TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
第1次握手,客户端向服务器端发出连接请求,等待服务器确认;第2次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;第3次握手,客户端再次向服务器端发送确认信息,确认连接。
完成三次握手并建立连接后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分泛,例如上传文件、下载文件、浏览网页等。
4.1 客户端
首先定义一个Client类,其中包含connect、send、receive、close方法,分别用于客户端的连接、客户端发送信息、客户端接收信息和关闭客户端socket。
import java.net.*;
import java.io.*;
//客户端程序
public class Client {
public Socket connect(InetAddress addr,int port) throws IOException {
// addr为要连接的服务器的ip地址,port为要连接的服务器端口;相对应的,服务器也应该监听此端口
Socket socket= new Socket(addr,port);
System.out.println("连接到服务器 "+socket.getInetAddress()+":"+socket.getPort());
return socket;
}
public void send(PrintWriter Socout,String line) {
Socout.println(line);
Socout.flush(); // 刷新
}
public String receive(BufferedReader SocBuf) throws IOException {
return SocBuf.readLine();
}
public void close(Socket socket) throws IOException {
socket.close();
}
}
在客户端主函数中,程序从系统输入读取信息并发送给服务器,然后将服务器端发送回来的翻译结果打印出来。当连续两次从系统输入“bye”时,客户端跳出循环,并终止程序。
public static void main(String[] args) throws Exception {
Client client=new Client();
BufferedReader SysBuf = new BufferedReader(new InputStreamReader(System.in));
//建立连接
Socket socket=client.connect(InetAddress.getByName("localhost"),10086);
BufferedReader SocBuf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter Socout = new PrintWriter(socket.getOutputStream());
//进行通信
String readline = SysBuf.readLine();
int k=0;
while(true){
client.send(Socout,readline);//发送
System.out.println(client.receive(SocBuf));//接收
if(readline.equals("bye")){
k=1;
System.out.println("如果你想退出,请再次输入“bye”,否则,该连接不会终止!");
}
readline = SysBuf.readLine();
if(readline.equals("bye")&&k==1){
client.send(Socout,readline);// 退出前告知服务器终止连接
client.close(socket);//关闭socket
break;
}
else k=0;
}
}
4.2 服务器端
首先定义一个Server类,其中包含connect、send、receive、close方法,分别用于服务器端绑定端口、服务器端发送信息、服务器端接收信息和关闭服务器端ServerSocket。
import java.net.*;
import java.io.*;
//服务器程序
public class Server {
public ServerSocket connect(int port) throws IOException {
return new ServerSocket(port);
}
public void send(PrintWriter Socout,String line) {
Socout.println(line);
Socout.flush(); // 刷新
}
public String receive(BufferedReader SocBuf) throws IOException {
return SocBuf.readLine();
}
public void close(Socket socket) throws IOException {
socket.close();
}
}
在服务器端主函数中,ServerSocket一直监听10086端口,当监听到连接请求时,返回一个Socket对象,并与其建立连接。连接建立后,服务器不断接收客户端发来的信息并交给Translators对象查询翻译结果,然后将得到的翻译结果发送回客户端。当服务器连续两次从客户端接收到“bye”时,服务器端跳出内层循环,并关闭当前连接的Socket,再次进入监听状态。
public static void main(String[] args)throws Exception{
Server server=new Server();
Translators t=new Translators();//翻译器
//建立连接
int port=10086;
ServerSocket s=server.connect(port);
while (true) {
System.out.println("正在监听"+port+"端口---");
Socket socket = s.accept();
System.out.println("连接到客户端" + socket.getInetAddress() + ":" + socket.getPort());
BufferedReader SocBuf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter Socout = new PrintWriter(socket.getOutputStream());
//通信
String readline = server.receive(SocBuf);// 如果SocBuf没有接收到信息,会一直处于暂停状态
int k = 0;
while (true) {
System.out.println( socket.getInetAddress() + ":" + socket.getPort()+"发送信息:"+readline);
if (readline.equals("bye")) k = 1;
server.send(Socout, readline + "的翻译结果是:" + t.translate(readline));//发送
readline = server.receive(SocBuf);//接收
if (readline.equals("bye") && k == 1) {
System.out.println(socket.getInetAddress() + ":" + socket.getPort()+"已断开连接!");
server.close(socket);
break;
} else k = 0;
}
}
}
4.3 远程词典调用
首先启动服务器端,然后启动客户端。
客户端界面如下所示:
连接到服务器 localhost/127.0.0.1:10086
中国
中国的翻译结果是:China
apple
apple的翻译结果是:苹果
拜拜
拜拜的翻译结果是:bye
bye
bye的翻译结果是:拜拜
如果你想退出,请再次输入“bye”,否则,该连接不会终止!
bye
Process finished with exit code 0
服务器端界面如下所示:
正在监听10086端口---
连接到客户端/127.0.0.1:57872
/127.0.0.1:57872发送信息:中国
/127.0.0.1:57872发送信息:apple
/127.0.0.1:57872发送信息:拜拜
/127.0.0.1:57872发送信息:bye
/127.0.0.1:57872已断开连接!
正在监听10086端口---
5. 服务器端使用多线程
在之前实现的几种方法中,服务器只能同时与一个客户端进行连接并通信。为了使服务器可以同时与多个客户端进行通信,可以在服务器端使用多线程的方式。
5.1 客户端
客户端程序与4.1节所示一致,不做任何修改。
5.2 服务器端多线程实现
首先实现一个ServerThread类,并重写run方法。
public class ServerThread implements Runnable {
private final Socket s;
private final Translators t=new Translators();//翻译器
public ServerThread(Socket s) {
this.s = s;
}
@Override
public void run() {
try {
BufferedReader SocBuf = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter Socout = new PrintWriter(s.getOutputStream());
//通信
String readline = SocBuf.readLine();// 如果SocBuf没有接收到信息,会一直处于暂停状态
int k=0;
while(true){
System.out.println( s.getInetAddress() + ":" + s.getPort()+"发送信息:"+readline);
if(readline.equals("bye")) k=1;
Socout.println(readline+"的翻译结果是:"+t.translate(readline));//发送
Socout.flush();
readline = SocBuf.readLine();//接收
if(readline.equals("bye")&&k==1) break;
else k=0;
}
System.out.println(s.getInetAddress() + ":" +s.getPort()+"已断开连接!");
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在服务器端主函数中,首先定义一个绑定到10086端口的ServerSocket对象,然后在一个死循环中监听该端口,如果监听到连接请求,则根据队列中返回的Socket对象开启一个线程,并再次进入监听状态。
public static void main(String[] args) throws IOException {
int port=10086;
ServerSocket s=new ServerSocket(port);
while(true){
System.out.println("正在监听"+port+"端口---");
Socket socket=s.accept();
System.out.println("连接到客户端"+socket.getInetAddress()+":"+socket.getPort());
new Thread(new ServerThread(socket)).start();//为每个客户端开启一个线程
}
}
5.3 远程词典调用
为了更好地展示多线程的效果,可以使用多个客户端对服务器发起连接请求。
客户端1窗口如下所示:
连接到服务器 localhost/127.0.0.1:10086
apple
apple的翻译结果是:苹果
bye
bye的翻译结果是:拜拜
如果你想退出,请再次输入“bye”,否则,该连接不会终止!
bye
Process finished with exit code 0
客户端2窗口如下所示:
连接到服务器 localhost/127.0.0.1:10086
中国
中国的翻译结果是:China
error
error的翻译结果是:null
bye
bye的翻译结果是:拜拜
如果你想退出,请再次输入“bye”,否则,该连接不会终止!
bye
Process finished with exit code 0
服务器端窗口如下所示:
正在监听10086端口---
连接到客户端/127.0.0.1:59298
正在监听10086端口---
连接到客户端/127.0.0.1:59304
正在监听10086端口---
/127.0.0.1:59298发送信息:apple
/127.0.0.1:59304发送信息:中国
/127.0.0.1:59304发送信息:error
/127.0.0.1:59304发送信息:bye
/127.0.0.1:59298发送信息:bye
/127.0.0.1:59298已断开连接!
/127.0.0.1:59304已断开连接!
6. 总结
使用UDP协议发送数据时,消耗资源小,通信效率高,但发送数据时不保证可靠性,有可能数据报丢失了,但发送方并不会重发。值得一提的是,使用UDP协议实现的接收端由于不需要与发送端建立连接,所以可以同时接收到多个客户端的发送信息并返回翻译结果,起到了类似多线程的效果。
使用TCP协议并在服务器端只使用简单的while(true)循环时(即不使用多线程),由于这种面向连接的特性,可以保证传输数据的安全,即便数据报丢失了,发送方也会重发。但是由于没有使用多线程,服务器只能同时与一个客户端进行通信。
使用TCP协议并在服务器端使用多线程时,服务器可以同时与多个客户端进行通信。但是当大量客户端进行请求时,服务器端会开启大量的线程与多个客户端进行通信,这会占用服务器过多的资源。可以限制服务器端的最大线程数,当有新的连接请求使得线程数超过阈值时,可以杀死最早的线程以保证线程数不会超过阈值。或者直接拒绝新的连接请求,只有当新的连接请求不会使线程数超过阈值时,才建立新的scoket并进行通信。