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

前一段时间,博主利用忙里偷闲的时间,对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多路复用的几种模式。
上面只是一个简单的SocketChannel示例,一次接受一个一个socket请求,接下来我们对上面的是示例进一步细化。
服务端ServerSocketChannel 示例:

package SocketChannel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

/**
 * @创建人:zhangzhiqiang
 * @创建时间:2019/09/03
 * @描述:
 * @联系方式:QQ:125717901
 **/

public class ServerSocketChannel2 {
    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();
                if(socketChannel!=null){
                    System.out.println("有新的客户端进来连接");
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (socketChannel.read(byteBuffer)!=-1){
                        byteBuffer.flip();
                            System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                            byteBuffer.clear();
                    }

                }


            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

客户端SocketChannel示例:

package SocketChannel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @创建人:zhangzhiqiang
 * @创建时间:2019/09/03
 * @描述:
 * @联系方式:QQ:125717901
 **/

public class SocketChannel2 {
    public static void main(String[] args) {

        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1",9000));
            socketChannel.configureBlocking(false);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            String str = null;
            while ((str=bufferedReader.readLine())!=null){
                ByteBuffer byteBuffer = ByteBuffer.allocate(str.length());
               byteBuffer.put(str.getBytes());
               byteBuffer.flip();
               socketChannel.write(byteBuffer);
               byteBuffer.clear();

            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里,我们服务端和客户端都是采用Channel的方式创建的,和传统的Socket不一样,服务端不通过的轮询是否有信息进来,如果有信息进来我们就创建一个ByteBuffer用来接收信息,然后在打印出来。服务端我们设置的非阻塞的方式,所以在下面接收到客户端socket的时候,需要进行一个判断,当前socket是否为null。
知识点:

  1. serverSocketChannel.configureBlocking(false); 设置通道的阻塞方式。
  2. if(socketChannel!=null) ;判断当前socket是否为空,如果不为空我们才能进行后续的业务操作。
  3. ByteBuffer.allocate(1024); 分配指定大小的ByteBuffer,ByteBuffer.allocateDirect(1024)分配一个直接的ByteBuffer效率更高,对于将一些文件读取到内存中处理来说可以使用该方法。
  4. byteBuffer.flip();用于将缓存区进行翻转,通过我们将数据写入Buffer中,如果想读取的话,一般我们采用byteBuffer.flip()方法将缓存区进行翻转,然后在读取缓存区的数据,其实这个byteBuffer.flip()方法并不是真的把缓存区翻了一个,他只是将buffer中的mark、position、limit进行重新标记了一下,方便数据读取。我们来看一下flip()方法的源码:
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

在这里我们可以看到,重置了position 和limit 。

  1. ByteBuffer数据打印问题,由于采用的Unicode编码,如果我们直接通过array方法打印可能出现乱码的问题。
  2. byteBuffer.clear();清除Buffer,在数据读取完毕后,我们一般会调用byteBuffer.clear();方便下次数据的写入,如果不调用的话,每次读数的数据都是在上次数据后的追加。其实byteBuffer.clear()方法也不是真的把buffer中的数据清除掉,他也是将buffer中的mark、position、limit进行重新标记了一下,方便下次数写入,我们来看一下源码:
   public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

他和flip()的区别就在与limit 的值,下面我们举例说明:
比如说我们在Buffer中有一个“helloword"字符串。并且我们Buffer的 capacity设置的为20,则:
当前buffer的各项值:
position = 9
limit = 20
capacity = 20
执行flip后:
position = 0
limit = 9
capacity = 20
执行clear 后:
position = 0
limit = 20
capacity = 20

package Buffer;

        import java.nio.ByteBuffer;
        import java.nio.CharBuffer;
        import java.nio.charset.CharacterCodingException;
        import java.nio.charset.Charset;

/**
 * @创建人:zhangzhiqiang
 * @创建时间:2019/09/03
 * @描述:
 * @联系方式:QQ:125717901
 **/

public class Buffer1 {
    public static void main(String[] args) {
        //1 将数据写到buff
        CharBuffer charBuffer = CharBuffer.allocate(20);
        charBuffer.put("helloword");
        System.out.println("capacity = "+charBuffer.capacity());
        System.out.println("limit = " +charBuffer.limit());
        System.out.println("position = "+charBuffer.position());

        charBuffer.flip();
        System.out.println("执行flip后:");
        System.out.println("capacity = "+charBuffer.capacity());
        System.out.println("limit = " +charBuffer.limit());
        System.out.println("position = "+charBuffer.position());

        System.out.println("执行clear后:");
        charBuffer.clear();
        System.out.println("capacity = "+charBuffer.capacity());
        System.out.println("limit = " +charBuffer.limit());
        System.out.println("position = "+charBuffer.position());
        System.out.println("打印buffer中的数据 = "+ charBuffer.clear());
    }
}

执行后的结果我们可以冲控制台看到和我们上面分析的结果一致,执行clear后,我们换是可以将buffer的数据打印出来的。


image.png

下面是position 和limit的一个图例:


image

通过上面的例子我们了解的CHannel、和Buffer的基本一些用法和原理,写了一个简单的例子,展示了一下客户端socket通过System的in方式获取数,然后发送到服务端,服务端是如何接受的,在这里的基础上,我们下一步引入selector选择器,如果是没有selector,我感觉Channel和传统的socket没有什么大的区别,并且意义不大。接下来我们引入selector,还是通过详细的示例一步一步给大家讲解。
。。。。。。。。。。。。。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342