java网络编程

198 阅读10分钟

一.网络模型及协议

OSI七层网络模型分为7层,应用层、表示层、会话层、传输层、网络层、链路层、物理层。网络数据会从应用层开始,往下通过各层,再经过物理层的传输后,再往上通过各层到达目标应用层。各层有不同的作用,如图所示。 C657CD85-3974-4AD2-8BD5-27AE9E61DC6B.png 各层对数据的格式有一些通用性的规范,也就是我们常说的协议。比如应用层的HTTP协议、FTP协议等,传输层的TCP协议、UDP协议等,网络层的IP协议等。

二.TCP和UDP协议

2.1 区别

  1. TCP是面向连接的、可靠的流协议。TCP通信的两台主机需要先建立连接才开始传输数据;传输过程采用“带重传的确认”机制保证传输的可靠性;采用“滑动窗口”的方式进行流量控制。
  2. UDP(用户数据报协议)是面向无连接的、不可靠的协议。发送数据时时直接发送,不管对方是否在接收,也不需要对方进行确认,所以不可靠,可能会有丢包现象。
  3. TCP适用于要求可靠传输场景,例如文件传输;UDP适用于实时场景,例如视频会议、直播等。

2.2 TCP三次握手

2.2.1 详细介绍

2D7B850E-0611-409B-B541-3CB866ADF94E.png

  1. 客户端给服务端发送请求建立连接的数据包,其中SYN(表示请求建立连接)=1,还有序列号seq=J(客户端假定的序号);发送后客户端处于SYB_SENT状态
  2. 服务端收到数据报后,发送应答客户端和请求建立连接的数据报,其中应答ACK=1,应答的序列号ack=J+1,请求连接连接的SYN=1,序列号seq=,K(服务端假定的序号);发送后服务端处于SYN_RCVD状态
  3. 客户端收到应答后,发送应答服务端的数据包,其中应答ACK=1,应答的序列号ack=K+1;发送后客户端处于连接建立状态 ESTABLISHED
  4. 服务端收到数据包后,检查应答序列号是否正确,正确的话变为连接建立状态 ESTABLISHED,3次握手成功,连接建立,可以进行数据传输

2.2.2 为什么要有三次握手

  1. TCP是面向连接的,所以需要双方都确认连接的建立;
  2. 第一次握手,客户端请求建立连接
  3. 第二次握手,服务端应答客户端,并请求建立连接
  4. 第三次握手,客户端对服务端请求进行确认应答,如果不进行第三次,服务端就不知道它的建立连接请求有没有被客户端接收

2.2.3 漏洞与防范

  • 可能的漏洞是SYN洪泛攻击:服务端会维护一个半开连接队列,大小有限;半开连接是在第二次握手后,会向第一次握手来源的ip发送数据包并等待对方的应答;当第一次握手的ip是伪造的假ip并且大量发出请求(第一次握手),半开连接队列会被占满,阻碍了正常的连接。
  • 解决方案是无效连接监控释放,对等待了一定时间的服务器连接进行释放;延缓TCB分配;防火墙,验证来源ip的有效性

2.3 TCP四次挥手

2.3.1 详细介绍

D4E3634E-A8E6-4CA8-877B-19906A1FD42F.png

  1. 第一次挥手:客户端发送关闭请求,不再向服务端传输数据
  2. 第二次挥手:服务端响应客户端的关闭请求,继续向客户端传输数据
  3. 第三次挥手:服务端发送关闭请求,不再向客户端传输数据
  4. 第四次挥手:客户端发送关闭请求请求,服务端收到数据报后就变为CLOSED状态,客户端发送后进入TIME-WAIT状态,经过一小段时间后再变为CLOSED状态

2.3.2 为什么要有四次挥手

TCP连接是全双工的,需要双方都停止数据传输,通知到对方,且进行单向的关闭

三.网络IO模型

3.1 同步与异步

同步与异步关注是否主动等待返回结果。

  1. 同步:调用方主动等待结果返回
  2. 异步:调用方不主动等待结果,而是通过回调函数、状态通知等方式拿到结果

3.2 阻塞与非阻塞

阻塞与非阻塞关注结果返回前调用方的状态。

  1. 阻塞:结果返回前,调用方线程挂起,什么都不做
  2. 非阻塞:结果返回前,调用方线程去做其他事,不挂起

3.3 两者组合

  1. 同步阻塞,最常用的模型,调用方什么都不做,等待结果返回
  2. 同步非阻塞,类似于轮询,调用方去做别的事,但偶尔来查看结果是否返回
  3. 异步阻塞,基本不用的模型,调用方确定了回调机制,但是不去做别的事
  4. 异步非阻塞,调用方确定了回调机制,然后去做别的事,回调触发时再去处理

3.4 数据传输流程

image.png 以部署的不同服务器的两个应用程序通讯为例:

  1. 应用A把消息发送到TCP发送缓冲区(写缓冲区)
  2. TCP把缓冲区的数据发送出去,经过网络传输后,发送到应用B的TCP接收缓冲区(读缓冲区)
  3. 应用B从缓冲区中读取属于自己的数据

3.5 五种网络IO模型

B89C20D1-1139-4283-9969-37784A6D97C7.png

  1. 阻塞I/O:调用方一直阻塞,等待接收缓冲区的数据
  2. 非阻塞I/O:调用方不阻塞,经常轮询检查接收缓冲区的数据,直到数据准备就绪
  3. I/O复用:调用方存在一些专门轮询检查缓冲区数据的线程,当数据准备就绪时,再通知数据处理的线程去处理。
  4. 信号驱动I/O:调用方存在一些信号处理线程,不去轮询,而是等待数据准备就绪之后给它发信号,再由它去通知数据处理的线程去处理
  5. 异步I/O:调用方的线程向内核注册关注的读事件,数据准备就绪后直接触发事件的处理

四.BIO实战

4.1 服务端

  1. 创建ServerSocket,绑定服务端监听端口
  2. 调用ServerSocket的accept()方法拿到与客户端的socket连接
  3. 拿到与客户端连接的socket的输入流,读取客户端传过来的数据
  4. 拿到与客户端连接的socket的输出流,把响应数据写入输出流,并flush把缓冲区的数据刷回客户端。
public class BIOServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        //1.绑定服务端监听端口
        serverSocket.bind(new InetSocketAddress(10001));
        System.out.println("server start...");
        //2.accept() 拿到与客户端的socket连接,包装成一个任务进行处理
        while (true){
            new Thread(new ServerTask(serverSocket.accept())).start();
        }
    }
    private static class ServerTask implements Runnable{
        public ServerTask(Socket socket) {
            this.socket = socket;
        }
        private Socket socket = null;
        @Override
        public void run() {
            try(ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){
                //3.从输入流中读取客户端数据
                String input = inputStream.readUTF();
                System.out.println("accept message:" + input);
                //4.把响应数据写入输出流,并flush把缓冲池数据刷到客户端
                outputStream.writeUTF("hello, " + input);
                outputStream.flush();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                try {
                    socket.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

4.2 客户端

  1. 创建Socket,并连接服务端
  2. 向服务端发送数据,并flush把缓冲区的数据刷到服务端
  3. 阻塞等待服务端的数据响应
public class BIOClient {

    public static void main(String[] args) {
        Socket socket = null;
        ObjectOutputStream output = null;
        ObjectInputStream input = null;
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1" ,10001);
        try {
            socket = new Socket();
            //1.连接服务器
            socket.connect(inetSocketAddress);
            output = new ObjectOutputStream(socket.getOutputStream());
            input = new ObjectInputStream(socket.getInputStream());
            //2.向服务器输出数据
            output.writeUTF("lm");
            output.flush();
            //3.接收服务器的响应
            String s = input.readUTF();
            System.out.println(s);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                if (socket != null){ socket.close();}
                if (input != null){ input.close();}
                if (output != null){ output.close();}
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

五.NIO实战

5.1 与BIO的区别

  1. 面向流和面向缓冲区,BIO是面向流的,NIO是面向缓冲区的,增加了处理过程的灵活性
  2. 阻塞与非阻塞,BIO是阻塞的,NIO是非阻塞的,从某通道读取数据,没数据时不阻塞,可以去做其他事;
  3. 选择器,NIO的选择器 允许一个线程监控多个输入通道

5.2 额外的概念

5.2.1 Selector,选择器

又叫事件订阅器;应用程序向Selector对象注册需要它关注的Channel,以及每个Channel对哪些IO事件感兴趣

5.2.2 Channels,通道

应用程序和操作系统交互事件、传递内容的渠道,可双向同时读写

5.2.3 buffer,缓冲区

通道中的数据总是要先读到一个Buffer,或者从一个Buffer中写入;buffer支持读写模式的切换

5.2.4 SelectionKey,事件类型

channel可以向Selector注册自己感兴趣的操作类型

  1. OP_ACCEPT,接受连接,仅服务端可用;当接收到一个客户端连接请求时就绪
  2. OP_CONNECT,请求连接,仅客户端可用;当SocketChannel.connect()请求连接成功后就绪
  3. OP_READ,读请求,一般都需要注册;当操作系统读缓冲区有数据可读时就绪
  4. OP_WRITE,写请求,一般没必要注册;当操作系统写缓冲区有空闲空间时就绪;一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该类型,否则该条件会不断就绪浪费CPU;当如果是写密集型的任务,比如文件下载,缓冲区很可能满,注册该类型就很有必要,要注意写完后取消注册

5.3 服务端

  1. 创建选择器和ServerSocketChannel
  2. ServerSocketChannel绑定端口,并注册到选择器中,关注接受连接事件 OP_ACCEPT
  3. 拿到选择器中的事件(selector.select()),遍历执行
  4. 如果有接受连接事件,说明有客户端申请建立连接,用ServerSocketChannel的accept方法拿到与客户端对应的SocketChannel,并注册到选择器,关注读事件 OP_READ
  5. 如果有读事件,数据途经SocketChannel写入到Buffer,flip切换buffer为读模式,把数据读到内存处理后再放入写缓冲区buffer,发送给SocketChannel
public class NioServerHandle implements Runnable{

    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;
    /**
     * 服务端构造方法
     * @param port 指定要监听的端口号
     */
    public NioServerHandle(int port) {
        try{
            //创建选择器
            selector = Selector.open();
            //打开监听通道
            serverChannel = ServerSocketChannel.open();
            //设置为非阻塞模式
            serverChannel.configureBlocking(false);
            //绑定端口
            serverChannel.socket().bind(new InetSocketAddress(port));
            //只关注请求连接事件
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            //标记服务器已开启
            started = true;
            System.out.println("服务器已启动,端口号:" + port);
        }catch(IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }
    public void stop(){
        started = false;
    }
    @Override
    public void run() {
        //循环遍历selector
        while(started){
            try{
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.
                selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    it.remove();
                    try{
                        handleInput(key);
                    }catch(Exception e){
                        if(key != null){
                            key.cancel();
                            if(key.channel() != null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch(Throwable t){
                t.printStackTrace();
            }
        }
        if(selector != null)
            try{
                selector.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
    }
    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            //处理新接入的请求连接事件
            if(key.isAcceptable()){
                ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                SocketChannel sc = ssc.accept();
                System.out.println("=======建立连接===");
                sc.configureBlocking(false);
                sc.register(selector,SelectionKey.OP_READ);
            }

            //处理读事件
            if(key.isReadable()){
                System.out.println("======socket channel 数据准备完成," +
                        "可以去读==读取=======");
                SocketChannel sc = (SocketChannel) key.channel();
                //创建缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //数据写入缓冲区
                int readBytes = sc.read(buffer);
                if(readBytes>0){
                    //切换为读模式
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String message = new String(bytes,"UTF-8");
                    System.out.println("服务器收到消息:" + message);
                    //处理数据
                    String result = response(message) ;
                    //发送应答消息
                    doWrite(sc,result);
                } else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }
            }
        }
    }
    //发送应答消息
    private void doWrite(SocketChannel channel,String response)
            throws IOException {
        byte[] bytes = response.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        //切换为写模式
        writeBuffer.flip();
        channel.write(writeBuffer);
    }
    //对请求数据进行响应
    private String response(String message){
        return "hello, " + message;
    }
}

5.4 客户端

  1. 创建选择器和SocketChannel
  2. SocketChannel异步连接服务器ip端口,注册到选择器,关注连接事件 OP_CONNECT
  3. 拿到选择器中的事件(selector.select()),遍历执行
  4. 如果有连接事件,说明连接建立成功,重新注册到选择器,关注 读事件 OP_READ
  5. 如果有读事件,数据途经SocketChannel写入到Buffer,flip切换buffer为读模式,把数据读到内存处理后再放入写缓冲区buffer,发送给SocketChannel
public class NioClientHandle implements Runnable{
    private String host;
    private int port;
    private volatile boolean started;
    private Selector selector;
    private SocketChannel socketChannel;

    /**
     * 客户端构造方法
     * @param ip 要连接的服务端的ip
     * @param port 要连接的服务端的端口
     */
    public NioClientHandle(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建选择器
            this.selector = Selector.open();
            //打开监听通道
            socketChannel = SocketChannel.open();
            //设置为非阻塞模式
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
    public void stop(){
        started = false;
    }

    @Override
    public void run() {
        //连接服务器
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        //循环遍历selector
        while(started){
            try {
                //阻塞方法,等待有关注的事件发生
                selector.select();
                //获取当前有哪些事件可以处理
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()){
                    key = it.next();
                    //删除处理过的事件
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }

        if(selector!=null){
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //具体的事件处理
    private void handleInput(SelectionKey key) throws IOException {
        if(key.isValid()){
            //获得关心当前事件的channel
            SocketChannel sc =(SocketChannel)key.channel();
            //处理连接事件
            if(key.isConnectable()){
                //连接成功的情况
                if(sc.finishConnect()){
                    System.out.println("=======与服务端建立连接===");
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else System.exit(-1);
            }

            //处理读事件
            if(key.isReadable()){
                //创建缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //数据写入缓冲区
                int readBytes = sc.read(buffer);
                if(readBytes>0){
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");
                    System.out.println("客户端收到消息:"+result);
                } else if(readBytes<0){
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    ///连接服务器
    private void doConnect() throws IOException {
        //连接不一定是立刻就能成功的
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            //立刻成功了,就直接关注读事件
            System.out.println("=======与服务端建立连接===");
            socketChannel.register(selector,SelectionKey.OP_READ);
        } else{
            //暂时还没成功,先关注连接事件
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }

    //向服务端发送数据
    public void sendMsg(String msg) throws IOException {
        doWrite(socketChannel,msg);
    }

    private void doWrite(SocketChannel sc,String request) throws IOException {
        byte[] bytes = request.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        sc.write(writeBuffer);
    }
}