网络编程学习笔记 - 01初见

90 阅读7分钟

写在前面: 前置的需要简单先了解下什么是Socket、长连接与短连接、网络通讯流程等,本文直接从JDK的BIO开始记录,并对RPC进行简单的实现。

JDK网络编程(BIO)

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。代码如下:

Client:

    public static void main(String[] args) throws IOException {
        // 客户端启动必备
        Socket socket = null;
        // 实例化与服务端通信的输入输出流
        ObjectOutputStream output = null;
        ObjectInputStream input = null;
        // 服务器的通信地址
        InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 10086);
        try {
            socket = new Socket();
            // 连接服务器
            socket.connect(addr);

            output = new ObjectOutputStream(socket.getOutputStream());
            input = new ObjectInputStream(socket.getInputStream());

            // 向服务器输出请求
            output.writeUTF("test");
            output.flush();

            // 接收服务器的输出
            System.out.println(input.readUTF());
        } finally {
            if (socket != null) {
                socket.close();
            }
            if (output != null) {
                output.close();
            }
            if (input != null) {
                input.close();
            }
        }
    }

Server:

public static void main(String[] args) throws IOException {
        /*服务器必备*/
        ServerSocket serverSocket = new ServerSocket();
        /*绑定监听端口*/
        serverSocket.bind(new InetSocketAddress(10086));
        System.out.println("Server start");

        while (true) {
            new Thread(new ServerTask(serverSocket.accept())).start();
        }
    }

    private static class ServerTask implements Runnable {

        private final Socket socket;

        public ServerTask(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            // 拿和客户端通讯的输入输出流
            try (
                    ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                    ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())
            ) {
                // 服务器的输入
                String userName = inputStream.readUTF();
                System.out.println(Thread.currentThread().getName() + ": Accept client message:" + userName);
                // response
                outputStream.writeUTF("Hello," + userName);
                outputStream.flush();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

Server输出日志:

Server start
Thread-0: Accept client message:test

Client输出日志

Hello, test

以上代码是传统BIO通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答模型,同时数据的读取写入也必须阻塞在一个线程内等待其完成。

该模型为一连接一线程的模型,如果用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型,底层还是使用的同步BIO,通常被称为“伪异步I/O模型”。

Server多线程版:

    private static final ExecutorService executorService
            = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public static void main(String[] args) throws IOException {
        // 服务端启动必备
        ServerSocket serverSocket = new ServerSocket();
        // 表示服务端在哪个端口上监听
        serverSocket.bind(new InetSocketAddress(10086));
        System.out.println("Server start");
        try {
            while (true) {
                executorService.execute(new ServerTask(serverSocket.accept()));
            }
        } finally {
            serverSocket.close();
        }
    }

    // 每个和客户端的通信都会打包成一个任务,交个一个线程来执行
    private static class ServerTask implements Runnable {

        private Socket socket;

        public ServerTask(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            // 实例化与客户端通信的输入输出流
            try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                 ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())) {

                // 接收客户端的输出,也就是服务器的输入
                String userName = inputStream.readUTF();
                System.out.println(Thread.currentThread().getName() + ":Accept client message:" + userName);

                // 服务器的输出,也就是客户端的输入
                outputStream.writeUTF("Hello, " + userName);
                outputStream.flush();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

Server日志输出:

Server start
pool-1-thread-1:Accept client message:test
pool-1-thread-2:Accept client message:test

RPC框架

什么是RPC?

RPC(Remote Procedure Call —— 远程过程调用),它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络的技术。RPC框架的目标就是要中间步骤都封装起来,让我们进行远程方法调用的时候感觉到就像在本地调用一样。

一次完整的RPC同步调用流程:

  • 服务消费方(client)以本地调用方式调用客户端存根。什么叫客户端存根?就是远程方法在本地的模拟对象,一样的也有方法名,也有方法参数,client stub接收到调用后负责将方法名、方法的参数等包装,并将包装后的信息通过网络发送到服务端;
  • 服务端收到消息后,交给代理存根在服务器的部分后进行解码为实际的方法名和参数
  • server stub根据解码结果调用服务器上本地的实际服务;
  • 本地服务执行并将结果返回给server stub;
  • server stub将返回结果打包成消息并发送至消费方;
  • client stub接收到消息,并进行解码;
  • 服务消费方得到最终结果。

RPC和HTTP

RPC字面意思就是远程过程调用,只是对不同应用间相互调用的一种描述,一种思想。具体怎么调用?实现方式可以是最直接的tcp通信,也可以是http方式,在很多的消息中间件的技术书籍里,甚至还有使用消息中间件来实现RPC调用的,我们知道的dubbo是基于tcp通信的,gRPC是Google公布的开源软件,基于最新的HTTP2.0协议,底层使用到了Netty框架的支持。所以总结来说,rpc和http是完全两个不同层级的东西,他们之间并没有什么可比性。

实现RPC框架

简单定义个接口:
public interface UserService {
    User findUserById(Integer id);
}

public class UserServiceImpl implements UserService {
    @Override
    public User findUserById(Integer id) {
        return new User(id,"test");
    }
}

public class User implements Serializable {
    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}

简单的RPC实现:

Client:

    // 远程调用类
    public static UserService getStub() {
        // 创建代理类
        InvocationHandler handler = (proxy, method, args) -> {
            Socket socket = new Socket("127.0.0.1", 10086);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(out);
            dos.writeInt(13);
            socket.getOutputStream().write(out.toByteArray());
            socket.getOutputStream().flush();

            DataInputStream dis = new DataInputStream(socket.getInputStream());
            int id = dis.readInt();
            String name = dis.readUTF();
            User user = new User(id, name);
            dos.close();
            socket.close();
            return user;
        };

        // 执行动态代理(传入类加载器、接口、代理对象,返回对象)
        Object o = Proxy.newProxyInstance(UserService.class.getClassLoader(), new Class[]{UserService.class}, handler);
        return (UserService) o;
    }

    public static void main(String[] args) throws Exception {
        UserService service = Client2.getStub();
        User user = service.findUserById(123);
        System.out.println(user.getName());

    }

Server:

    public static void main(String[] args) throws  Exception{
        ServerSocket serverSocket = new ServerSocket(10086);
        while (true){
            Socket socket = serverSocket.accept();
            process(socket);
            socket.close();
        }
    }
    private static void  process(Socket socket) throws Exception{
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        DataInputStream dataInputStream = new DataInputStream(in);
        DataOutputStream dataOutputStream = new DataOutputStream(out);
        int id = dataInputStream.readInt();
        UserService service =  new UserServiceImpl();
        User user = service.findUserById(id);
        dataOutputStream.writeInt(user.getId());
        dataOutputStream.writeUTF(user.getName());
        dataOutputStream.flush();
    }

被调用的服务本质上是远程的服务,但是调用者不知道也不关心,调用者只要结果,具体的事情由代理的那个对象来负责这件事,此场景适合用代理模式。

代理(Proxy)即通过代理对象访问目标对象,这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。这里额外的功能操作是通过网络访问远程服务。上面的代码为动态代理。

考虑序列化问题

序列化问题在于我们的方法调用,有方法名,方法参数,这些可能是字符串,可能是我们自己定义的java的类,但是在网络上传输或者保存在硬盘的时候,要进行序列化和反序列化。java里已经为我们提供了相关的机制Serializable。

理用反射机制再进行一次实现:

Client:

    public static Object getStub(final Class<?> clazz) {
        InvocationHandler handler = (proxy, method, args) -> {
            Socket socket = new Socket("127.0.0.1", 10086);
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            String className = clazz.getName();
            String methodName = method.getName();
            Class[] parametersTypes = method.getParameterTypes();
            // 自己定义的协议(className|methodName|parametersTypes|args)
            oos.writeUTF(className);
            oos.writeUTF(methodName);
            oos.writeObject(parametersTypes);
            oos.writeObject(args);
            oos.flush();
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Object o = ois.readObject();
            oos.close();
            socket.close();
            return o ;
        };
        return Proxy.newProxyInstance(clazz.getClassLoader(),
                new Class[]{clazz},handler);
    }

    public static void main(String[] args) {
        UserService service = (UserService) Client3.getStub(UserService.class);;
        User user = service.findUserById(123);
        System.out.println(user.getName());
    }

Server:

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(10086);
        while (true) {
            Socket socket = serverSocket.accept();
            process(socket);
            socket.close();
        }
    }

    private static void process(Socket socket) throws Exception {
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        ObjectInputStream ois = new ObjectInputStream(in);

        // 自己定义的协议(className|methodName|parametersTypes|args)

        String clazzName = ois.readUTF();
        String methodName = ois.readUTF();
        Class<?>[] parameterTypes = (Class<?>[]) ois.readObject();
        Object[] args = (Object[]) ois.readObject();
        //反射拿到class
        Class<?> clazz = Class.forName(clazzName);
        if (clazz.isInterface()) {
            if (clazzName.equals("org.study.rpc.UserService")) {
                clazz = UserServiceImpl.class;
            }
            // 这里可以使用反射机制拿到所有接口对应的实现类
        }

        Method method = clazz.getMethod(methodName, parameterTypes);

        Object object = method.invoke(clazz.newInstance(), args);
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(object);
        oos.flush();
    }

当然,这都是很简单很基础的实现,再次进阶还需要考虑的问题:

  • 性能欠缺,表现在网络通信机制,序列化机制等等;
  • 负载均衡、容灾和集群功能很弱;
  • 服务的注册和发现机制也很差劲。

可以看看Dubbo的源码进一步学习。