基于 socket 手写一个 TCP 服务端及客户端

  通过 socket 实现一个 TCP 服务端与客户端,实现通过 TCP 协议进行消息收发。

  关键在 socket 的使用的理解上。

  socket 是对操作系统提供的协议栈的封装,底层调用的是操作系统提供的协议栈。

  当我们调用 ServerSocket 的 accept 方法时,线程阻塞。以 TCP 协议为例,直到网卡接收到一个三次握手的连接请求,网卡向 CPU 发送中断信号,CPU 调用中断处理程序唤醒我们阻塞在 accept 方法上的线程,进行连接处理。

  三次握手的过程是由协议栈完成的,我们在应用层编程无法感知。直到三次握手完成,协议栈将客户端信息与服务端信息封装在一个 Socket 对象中返回,我们通过该Socket 对象完成数据的收发。

  值得注意的是,在连接建立完成前,操作系统会为本次连接在内核空间开辟两个数据缓冲区:发送缓冲区与接收缓冲区。

  我们要做的是监听发送缓冲区是否有数据到达,以及将需要发送的数据写入发送缓冲区。

  至于网卡接收到的数据何时由操作系统拆包并写入接收缓冲区,以及我们写入发送缓冲区的数据何时会被封装为 TCP 报文发送给网卡是操作系统 OS 控制的,这对我们来说是透明的。不同的 OS 对此会有不同的实现,我们不需要关注这些细节(或者说想要关注也没办法介入)。

  本次实现是通过传统的 ServerSocket 建立服务端,并没有使用通道技术。也就是说是 BIO 的实现,当并发量比较大时可以采用 NIO 多路复用技术进行优化。这可以帮助我们节约线程数。

  对 TCP 报文进行分包有多种方式,本次实现使用的是最普适的方式,通过报文头添加报文长度字段进行分包,也就是与 HTTP 协议 Header 中的 Content-Length 相同的方式。

  测试时客户端发送的数据是一个序列化的对象,服务端对其进行反序列化并检查结果。 

  由于牵扯到线程的切换,本次实现并没有对代码结构进行提前设计,仅仅是简单的实现了数据收发功能。经过设计优化的代码将在下篇博客发出。

  服务端:

/**
 * @Author Nxy
 * @Date 2020/3/21 17:16
 * @Description socket 服务端
 */
public class BasicSeverDemo {
    public static void main(String[] args) {
        ServerSocket server = null;
        try {
            server = new ServerSocket(80);
            System.out.println("server start!");
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        while (!Thread.currentThread().isInterrupted()) {
            Socket socket;
            BufferedInputStream in;
            BufferedOutputStream out;
            try {
                //阻塞等待连接请求
                socket = server.accept();
                System.out.println("建立连接:" + socket.getInetAddress());
                in = new BufferedInputStream(socket.getInputStream());
                out = new BufferedOutputStream(socket.getOutputStream());
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("连接建立失败!");
                continue;
            }
            byte[] result;
            try {
                //阻塞等待接收请求数据
                byte[] lengthByte = IOUtil.readBytesFromInputStream(in, 4);
                //本次请求的长度
                int length = ByteBuffer.wrap(lengthByte).getInt();
                System.out.println("from server:" + length);
                //读取指定长度字节
                result = IOUtil.readBytesFromInputStream(in, length);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
            //反序列化对象
            Invocation obj = null;
            try {
                ByteArrayInputStream bis = new ByteArrayInputStream(result);
                ObjectInputStream ois = new ObjectInputStream(bis);
                obj = (Invocation) ois.readObject();
                ois.close();
                bis.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }
            System.out.println(obj.getInterfaceName() + ":" + obj.getMethodName());
        }
    }

  客户端:

/**
 * @Author Nxy
 * @Date 2020/3/21 17:54
 * @Description socket 客户端
 */
public class BasicClientDemo {
    public static void main(String[] args) {
        Socket socket;
        BufferedOutputStream out;
        BufferedInputStream in;
        try {
            socket = new Socket("127.0.0.1", 80);
            out = new BufferedOutputStream(socket.getOutputStream());
            in = new BufferedInputStream(socket.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        Object[] params = new Object[2];
        Class[] paramTypes = new Class[2];
        Invocation invocation = new Invocation(BasicClientDemo.class.getName(), "main", paramTypes, params);
        byte[] invocationBytes = toByteArray(invocation);
        int length = invocationBytes.length;
        try {
            System.out.println("from client:" + length);
            out.write(ByteBuffer.allocate(4).putInt(length).array());
            out.flush();
            out.write(invocationBytes);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                out.flush();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

  工具类:

public class IOUtil {
    /**
     * @Author Nxy
     * @Date 2020/3/21 20:21
     * @Param in:输入流,length:读取字节数
     * @Return
     * @Exception
     * @Description 从输入流读取指定长度字节的数据
     */
    public static byte[] readBytesFromInputStream(BufferedInputStream in,
                                                  int length) throws IOException {
        int readSize;
        byte[] bytes = null;
        bytes = new byte[length];
        long length_tmp = length;
        long index = 0;// start from zero
        while ((readSize = in.read(bytes, (int) index, (int) length_tmp)) != -1) {
            length_tmp -= readSize;
            if (length_tmp == 0) {
                break;
            }
            index = index + readSize;
        }
        return bytes;
    }

    public static byte[] toByteArray(Object obj) {
        byte[] bytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();
            bytes = bos.toByteArray();
            oos.close();
            bos.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return bytes;
    }
}

  执行效果,服务端正常接收到数据并成功反序列化为对象:

 

posted @ 2020-03-21 21:02  牛有肉  阅读(1272)  评论(0编辑  收藏  举报