美文网首页java面试
Java socket详解,看这一篇就够了续

Java socket详解,看这一篇就够了续

作者: 长道 | 来源:发表于2019-08-29 17:39 被阅读0次

    前一段时间,博主利用忙里偷闲的时间,对Java Socket通信进行了一个简单的描述,由浅入深,循序渐进的将Java socket基本核心通过完善的小示例讲解了一下,根据部分网友的反映,虽然没到达到立竿见影的效果,但对需要进一步了解socket通信的网友来说,至少可以说是雪中送炭、锦上添花吧。

    其次,根据部分网上的反映,对代码的可读性比较差,不够美观的问题,在这里解释一下,代码是由idea直接拷贝过来的,为了方便网友可以直接在本地运行,然后观察具体的运行结果,有助于进一步加深理解,在后续的章节中,我会将代码的可读性进一步完善,提高代码的可读性,在这里同时多谢网友提出的宝贵意见。
    接下来我们将进一步讨论Java socket 异步通信,Java socket异步通信包主要位于是在Java.nio框架下,在讲解Java socket异步通信前,我们先来回顾一下传统socket通信的演进。
    Blocking I/O 模式

    image

    BlockIng I/O模式下,主要缺点如下:

    1. 只能用于小规模下多个socket通信,因为客户端socket每次连接请求后,服务端ServerSocket都会创建一个线程来处理当前客户端的连接请求,如果连接数非常大,以千万级为单位,那么服务端的CPU资源开销会是一个非常庞大的数据。

    2. Read、Write读写资源问题,由于是阻塞的读写模式,如果大量线程处于空闲状态没有数据可读写,则会造成空闲socket的Read 、Write操作大量阻塞,对系统资源线程的开销也会造成非常大的浪费。

    接下来我们看看NIO(not Blocking I/O ,也有人叫他new IO)的工作原理,NIO主要实现机制于IO最大的区别在他通过选择器与采用观察者模式将之前大量连接采用一个线程即可搞定,同时通过通道的方式,可对流进行重复选择的读取,下面我们通过图形来描述一下NIO的工作原理。

    image

    NIO模式下下的优点:

    1. NIO采用channel 与selector结合方式,可以多次从通过读写或者写入通道数据,并且可以读取指定位置的数据,而传统io方式,采用流的方式对数据进行读取,一但打开流,那么只能读取到流的结尾,无法从流指定的位置进行读取。我们把数据流比作打开的自来水管一样,你没法只获取水流中的一部分数据。

    2. socketor选择器,在通过socketchannl将socket注册到选择器中,那么就可以通过一个线程处理注册进来的所有socket。socketor说的通俗一点就像饭店的点菜系统,比如说在传统上,我们点菜的流程是这样的,拿着菜单,把服务员叫过来,你在点菜,服务员在旁边候着,形成的方式是客户和服务员一对一的方式,如果饭店只有10个服务员,那么我只能服务10个用户,这样是效率及其底下的。而使用socketor后,在你点菜的时候,服务员给你一个电子菜单(或者像海底捞的纸质可以选择的菜单),你自己将需要点的菜在菜单上勾选,点好了直接给服务员就可以了,这样加入饭店来了100个客人,那个10个服务员就只需要将菜单发给客户,客户自己选择菜名后,交给服务员即可。

    3. 我们知道流的数据是单向的,而socketChannel则是双向的,我们继可以向通道中写数据,也可以从通道中读取数据,并且通道中的数据读写都是通过buffer实现的。

    上面我们简单的介绍了一下NIO中socket的应用原理,接下来我们详细介绍一下NIO中socket相关的知识点,由于NIO框架下涉及的类和接口非常多,在这里我们主要讲解的是nio下的socket通信,所有我把nio下的关于socket相关的主要的几个类和接口进行整理和分类一下,方便大家有个脉络,其实,我们分析一下,nio下和socket通信相关的我们可以把大分为三大类(其实应该是俩类,channel 与buffer,selector相关的也是在channel下,在这里是主要是为了给大家讲解的清楚,我把selector拿出来了,进行单独的分类),channel、buffer与selector三大类,每一种类型下面涉及到常用的类和接口我在大家整理一下,请看下面的这个思维导图:

    image

    首先我们来看一下buffer、channel与selector这三者之间的区别和联系,channel通道,这里我们可以把它理解为传统io的流,而buffer就是针对channel 的一个缓冲区,他就是一个连续的内存块,是NIO数据的一个中转站。我们可以将channel中的数据读取到buffer中,也可以将buffer中的数据写入到channel中,所以channel是双向的,可以进行读写操作,而传统IO基于字节流的操作,读和写都是分开的,我们必须打开对应Input才可以操作IO。

    接下来,我们首先看channel包中的这几个核心的类。在这里我主要介绍一下服务端的socketChannel 与客户端的socketchannel。其他的类大家可以自行阅读API,结合源码我详细有更深入的了解。

    ServerSocketChannel 类是有常用的几个方法分别是:

    1. abstract SocketChannel accept() 。接受来之Channel通道socket的连接。

    2. ServerSocketChannel bind(SocketAddress local)。将通道的socket绑定到本地地址。

    3. abstract ServerSocketChannel bind(SocketAddresslocal, int backlog)。是上一个方法的重载,也是刚通道的socket绑定到本地地址,第一个参数是本地地址,第二个表示挂起连接数的最大值。

    4. abstract SocketAddress getLocalAddress()。返回当前通道socket绑定的本地地址

    5. static ServerSocketChannel open()。 打开一个ServersocketChannel。

    6. abstract ServerSocket socket()。检索通道相关联的socket

    由于ServerSocketChannel 继承了ServerSocketChannel 并且实现了NetworkChannel 的接口,所有他换有一些其他的方法可用,比如:

    7. void close()。 关闭通道的方法。

    8. abstract SelectableChannel configureBlocking(boolean block)。调整通道的阻塞模式。

    9.SelectionKey register(Selectorsel, int ops)。将通道注册到制定的选择器上,

    1. SelectorProvider provider()。返回创建通道的提供程序

    SocketChannel 类是有常用的几个方法分别是:

    1. abstract SocketChannel bind(SocketAddresslocal)。 将通道的socket绑定到本地地址。

    2. abstract boolean connect(SocketAddress remote)。

    3. abstract SocketAddress getLocalAddress()。

    4. abstract SocketAddress getRemoteAddress()。

    5. abstract boolean isConnected()。

    6. static SocketChannel open()。 打开一个socketChannel

    1. static SocketChannel open(SocketAddress remote)。

    8.abstract Socket socket()。 检索与通道相关联的socket

    1. abstract SocketChannels hutdownInput()。 在不关闭通道的情况下,关闭连接已方便获取数据。

    Selector 类是有常用的几个方法分别是:

    1. abstract void close() 。 关闭当前选择器

    2. abstract boolean isOpen()。 当前选择器是否打开

    3. abstract Set<SelectionKey> keys()。返回当前选择器中的key,是一个set集合

    4. static Selector open()。 打开一个选择器。

    5. abstract int select()。当对的通道io准备好时选择一组键,

    6. abstract Set<SelectionKey> selectedKeys()。返回当前选择器的selected-key set.集合

    SelectionKey类有四个属性,分别是:

    static int OP_ACCEPT 接受socket

    static int OP_CONNECT 开始连接

    static int OP_READ 开始读数据

    static int OP_WRITE 开始写数据

    同时也有对应的几个方法。分别是isAcceptable()、isConnectable()、isReadable() 和isWritable()。

    上面我们对常用的几个接口和方法进行进行了详细的介绍,接下来我们就通过详细的例子抽丝剥茧了解他们的原理,

    先来一个简单的例子:
    ServerSocketChannel 服务端.

    
    package SocketChannel;
    
    import java.io.IOException;
    
    import java.net.InetSocketAddress;
    
    import java.nio.ByteBuffer;
    
    import java.nio.channels.ServerSocketChannel;
    
    import java.nio.channels.SocketChannel;
    
    import java.nio.charset.Charset;
    
    public class ServerSocketChnnel1 {
    
    public static void main(String[] args) {
    
    try {
    
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
                serverSocketChannel.bind(new InetSocketAddress(9000));
    
                serverSocketChannel.configureBlocking(false);
    
                while (true){
    
    SocketChannel socketChannel = serverSocketChannel.accept();
    
                    while (socketChannel!=null){
    
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    
                        int i = socketChannel.read(byteBuffer);
    
                        byteBuffer.flip();
    
                        System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
    
    .toString());
    
                    }
    
    }
    
    }catch (IOException e) {
    
    e.printStackTrace();
    
            }
    
    }
    
    }
    
    

    客户端:

    package socket;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.util.Date;
    
    public class ClientSocket {
    
        public static void main(String[] args) {
            Socket socket;
    
            {
                try {
                    socket = new Socket("127.0.0.1",9000);
                    OutputStream outputStream = socket.getOutputStream();
                    outputStream.write("你好".getBytes());
                    outputStream.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
    }
    
    

    运行后我们可以看到控制台收到了客户端的信息。


    image.png

    错误,我们先不需要关注,后面我们一步一步带大家讲解。
    首先我们分析服务端程序:
    第一步:通过ServerSocketCHannel.open()打开这个Channel通道,我们看一下他这个源码:

       public static ServerSocketChannel open() throws IOException {
            return SelectorProvider.provider().openServerSocketChannel();
        }
    

    他是调用SelectorProvider类的provider()方法,获取SelectorProvider,然后在调用SelectorProvider的openServerSocketChannel()方法。其中provider()方法是一个线程安全的。

     public static SelectorProvider provider() {
            synchronized (lock) {
                if (provider != null)
                    return provider;
                return AccessController.doPrivileged(
                    new PrivilegedAction<SelectorProvider>() {
                        public SelectorProvider run() {
                                if (loadProviderFromProperty())
                                    return provider;
                                if (loadProviderAsService())
                                    return provider;
                                provider = sun.nio.ch.DefaultSelectorProvider.create();
                                return provider;
                            }
                        });
            }
        }
    

    通过上面我们可以看到,open()方法打开一个线程安全的ServerSocketChannel。
    第二步:我们通过bind()方法绑定对应的端口。这个和我们普通的ServerSocket类似。
    第三步:通过configureBlocking()方法设置阻塞方式。
    第四步:就可以通过accept方法接受对应的请求了。
    通过上面的小例子,我们简单的描述了一下ServerSocketChannel最基本的概念和应用,让大家有一个初步的认识,那么在接下来的示例中,我会引入Selector 选择器、ByteBuffer缓存、已经IO多路复用的几种模式。
    待续。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

    相关文章

      网友评论

        本文标题:Java socket详解,看这一篇就够了续

        本文链接:https://www.haomeiwen.com/subject/dwcwectx.html