Socket实现远程方法调用(RMI)

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并进行通信。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冷冰殇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值