网络通信Socket学习记录

文章详细介绍了Socket在网络通信中的作用,从Unix的文件描述符概念出发,阐述了TCP/IP协议栈的层次结构。接着,讨论了阻塞I/O和非阻塞I/O的工作模式,展示了Java中如何实现这两种模式的服务器和客户端示例。此外,文章还提到了新的I/O模型,如多路复用器(Selector)在提高并发性能方面的作用。
摘要由CSDN通过智能技术生成

网络通信Socket

Socket

  • socket起源于unix,而unix/linux基本思想就是一切皆文件,也称为文件描述符
  • socket是对“open—write/read—close”的一种实现
  • socket是对TCP/IP协议的一种封装,socket本身不是协议,通过socket才能使用TCP/IP协议

网络I/O

  • 本地I/O

    数据 -----> 从磁盘读取到内核空间的缓冲区----> 内核空间的缓冲区读取到用户空间的缓冲区

  • 网络I/O
    数据----> 从网卡读取到内核空间的缓冲区----> 内核空间的缓冲区读取到用户空间的缓冲区

TCP/IP协议

应用层(Socket接口在这里)、传输层、网络层、数据链路层

阻塞I/O(BlockingIO)

  1. 应用程序发起一个系统调用
  2. 内核中数据包准备中,需要等待到数据包准备好
  3. 将数据从内核拷贝到用户空间
  4. 再返回给应用程序。

这是一个完整的请求,这4步中所等待的时间都是阻塞的。

socket连接四要素:源IP、目标IP、源端口、目标端口

public class BlockingIOServer {
    public static void main(String[] args) throws IOException {
        // 我们首先通过ServerSocket来监听端口,我们知道,每个进程都有一个唯一的端口
        ServerSocket serverSocket = new ServerSocket(8080);
        // 然后通过accept方法阻塞调用,直到有客户端的连接过来,就会返回Socket
        Socket socket = serverSocket.accept();
        // 然后获取Socket的输入流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 获取客户端的数据,这个地方是一个阻塞的IO,阻塞到直到数据读取完成
        String clientStr = bufferedReader.readLine();
        System.err.println("收到客户端数据:" + clientStr);
        // 获取Socket的输出流
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        // 给客户端回写数据
        bufferedWriter.write("ok\n");
        // 最后刷新
        bufferedWriter.flush();
    }
}

public class BlockingIOClient {
    public static void main(String[] args) throws IOException {
        // 创建一个Socket连接,访问localhost,8080端口的服务端
        Socket socket = new Socket("localhost", 8080);
        // 获取socket的输出流,把数据写入到服务端
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        // 客户端向服务端数据写入,一定要有数据结束符,不然服务端不知道自己读取完成没有
        bufferedWriter.write("client receive msg !\n");
        bufferedWriter.flush();
        // 获取socket的输入流,此处是获取服务端给客户端回写的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 读取数据
        String receiveMsg = bufferedReader.readLine();
        System.out.println("receiveMsg:" + receiveMsg);
    }
}

非阻塞I/O(No Blocking I/O)

  1. 应用程序发起一个系统调用

  2. 内核中数据包准备中,反复轮询直到数据准备好

  3. 将数据从内核拷贝到用户空间

  4. 再返回给应用程序。

    这种情况下会有多个client去连接一个server,而server端每一次的read也会消耗一定的资源。

    package com.yt.study.socket.newblockingio;
    
    import java.io.*;
    import java.net.*;
    import java.nio.*;
    import java.nio.channels.*;
    import java.util.*;
    
    public class NewBlockingIOServer {
        public static List<SocketChannel> clients = new ArrayList<>(8);
    
        public static void main(String[] args) throws IOException, InterruptedException {
            // 得到一个serverSocketChannel管道,这个就等同于ServerSocket,只不过这个是支持异步,并且可同时读写。
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 设置socket为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 绑定端口
            serverSocketChannel.bind(new InetSocketAddress(8080));
            while (true) {
                // 接收客户端的请求,调用accept,由于设置成非阻塞了,所以accept将不会阻塞在这里等待客户端的连接过来
                SocketChannel socketChannel = serverSocketChannel.accept();
                // 不阻塞之后,得到的这个socketChannel就有可能为空
                if (null != socketChannel) {
                    // 同时也设置socketChannel为非阻塞,因为原来我们读取数据read方法也是阻塞的
                    socketChannel.configureBlocking(false);
                    clients.add(socketChannel);
                } else {
                    Thread.sleep(3000);
                    System.err.println("没有连接过来,继续等待!!!");
                }
                clients.forEach(client -> {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    try {
                        int num = client.read(byteBuffer);
                        if (num > 0) {
                            System.out.println("客户端端口:" + client.socket().getPort() + ",收到客户端数据:" + new String(byteBuffer.array()));
                        } else {
                            System.out.println("等待客户端写数据!!!");
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
    
    

new I/O

  • channel:介于字节缓存区和套接字之间,可以同时读写,支持异步IO
  • buffer:字节缓冲区,是应用程序和通道直接进行IO数据传输的中转
  • selector:多路复用器,监听服务端和客户端的管道上注册的事件

多路复用:多个请求组过来形成了多个管道(多个IO)复用一个系统调用,由之前的read变成了selector。

public class NewIOServer {
    static Selector selector;

    public static void main(String[] args) {
        try {
            // 得到一个多路复用器
            selector = Selector.open();
            // 获取一个管道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 把连接事件注册到多路复用器上,通过注册不同事件处理不同的任务,把serverSocketChannel注册
            // 到selector上,主要是当连接到来的时候,由于有一个Accept事件,那么根据Accept事件接受到客户端的SocketChannel
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> iterable = set.iterator();
                while (iterable.hasNext()) {
                    SelectionKey key = iterable.next();
                    iterable.remove();
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleAccept(SelectionKey key) {
        // 从selector中获取ServerSocketChannel,因为当初把ServerSocketChannel注册再selector上,并且注册的accept事件
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        try {
            // 能到这里,一定时有客户端连接过来,所以一定会有连接
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 设置为非阻塞
            socketChannel.configureBlocking(false);
            // 会写数据
            socketChannel.write(ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
            // 然后注册read事件,等while的循环再次获取read事件,然后读取SocketChannel中的数据
            socketChannel.register(selector, SelectionKey.OP_READ);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleRead(SelectionKey key) {
        // 获取SocketChannel
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            // 读取客户端的数据,这个里面不一定有值
            socketChannel.read(byteBuffer);
            System.err.println("receive msg : " + new String(byteBuffer.array()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }  
}

public class NewIOClient {
    static Selector selector;

    public static void main(String[] args) {
        try {
            selector = Selector.open();
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                selector.select();
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> iterator = set.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isConnectable()) {
                        handleConnect(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleConnect(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        if (socketChannel.isConnectionPending()) {
            socketChannel.finishConnect();
        }
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("Hello server!!!! ".getBytes(StandardCharsets.UTF_8)));
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel socketChannel= (SocketChannel) key.channel();
        ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.err.println("client receive msg : " + new String(byteBuffer.array()));
    }
}

Socket底层

Socket是一个文件描述符,其实说白了Socket就是一个文件流,在linux下Socket就是文件的形式存在,那么网络IO是从文件中读取数据,只不过这个文件是一个Socket文件,java就是操作这个Socket文件的文件描述符,写入和读出都是通过文件描述符。
每一个进程都有一个数据结构task struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符,意味着任何一个进程都有对应的文件,文件描述符就是一个整数,是这个数组的下标。所以操作socket其实就是操作这个文件描述符,所以我们经常也听到Socket是一个文件描述符,但是这样子描述并不是很准确。虽然说Socket是文件,但是不会像真正的文件一样保存在磁盘中,而是保存在内存里。每个socket都是有接收队里,发送队列还有等待队列组成,队
列又是由Sk_buffer这样的一个数据结构保存数据的。进程的数据结构里面有一个文件的集合,然后Socket其实就是放在这个集合中

在这里插入图片描述

Socket是对“open—write/read—close”的一种实现

Socket是对TCP/IP的封装

select模型

假设现在有三个已经准备好了的socket连接,等待数据传输,那么想要一个线程去处理多个socket,在这个socket都是有接受队列、发送队列、等待队列。

所以每次调用selector.select()方法时,都会把线程的引用放到所有socket的等待队列中,当接收到客户端的数据后,把每个socket上的等待队列中的线程移除,并放入就绪队列中,然后去遍历文件列表,找出所有接收到数据的socket。

在这里插入图片描述

缺点:

  1. 每次调用select都需要将线程加入到所有socket对象的等待队列中,每次唤醒进程又要将线程从所有socket对象的等待队列中移除。这里涉及到对socket列表的两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数 量,默认只能监视1024个socket(强行修改也是可以的);
  2. 程被唤醒后,程序并不知道socket列表中的那些socket上收到数据,因此在用户空间内需要对socket 列表再做一次遍历。

poll模型

poll模型和select相似,只是对监听的socket没有限制。 select模型和poll模型其实每次都是遍历所有的socket,有些socket其实没有事件,还是会去遍历,如果socket越多,那么遍历时间就越长,在高并发的情况下,select模型的效率其实比较低,那么有没有一种模型,可以只返回有事件的socket呢,而不需要遍历那么多的socket,答案就是epoll模型。

epoll模型

涉及到C语言的一些代码,实在是没看明白。就不做介绍了。

优点:

1.支持一个进程打开很大数目的socket描述符

2.IO效率不随FD数目增加而线性下降

同步、异步

同步:应用程序发起了请求后,后边还一直由这个应用程序进行询问,直到返回了数据。

异步:应用程序发起了请求后,后续就不管了,而由内核主动告诉你,并主动返回数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值