Java Socke 探究

Java中的Socket能够分为普通Socket和NioSocket两种。

普通Socket的用法

Java中的网络通讯是经过Socket实现的,Socket分为ServerSocket和Socket两大类,ServerSocket用于服务端,能够经过accept方法监听请求,监听到请求后返回Socket,Socket用于具体完成数据传输,客户端直接使用Socket发起请求并传输数据。java

一个简单的交互介绍ServerSocket及Socket的使用:

1. 建立ServerSocket。

ServerSocket的构造方法一共有5个。最方便的是传入一个端口参数的方法。

2. 调用建立出来的ServerSocket的accept方法进行监听

accept方法是阻塞方法,也就是说调用accept方法后程序会停下来等待链接请求,在接收到请求以前程序将不会继续执行,当接收到请求以后,accept方法会返回一个Socket。

3. 使用accept方法返回的Socket与客户端进行通讯。

服务端代码示例:segmentfault

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Server {

    public static void main(String[] args) {

        try {
            //建立一个ServeSocket,设置端口为8080
            ServerSocket serverSocket = new ServerSocket(8080);
            //运行Socket监听,等待请求  此方法会阻塞线程,当有请求时才会继续执行
            Socket socket = serverSocket.accept();
            //接收到请求以后使用Socket进行通讯,建立BufferedReader用于读取请求的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream());
            String line = in.readLine();
            System.out.println(line);
            //建立PrintlnWriter,用于发送数据
            out.println("已经接受到了数据");
            out.flush();
            System.out.println("Server关闭" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(new Date()));
            //关闭资源
            out.close();
            in.close();
            socket.close();
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

客户端代码示例:网络

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;


/**
 * 客户端
 * @author sanchan
 */
public class Client {
    public static void main(String[] args) {
        //须要先启动Server不然报错java.net.ConnectException: Connection refused
        try {
            String msg="你好,ServerSocket!";
            //建立一个Socket,与本机8080端口链接
            Socket socket=new Socket("127.0.0.1",8080);
            //使用Socket建立PrintWriter和BufferedReader进行数据的读写
            PrintWriter out=new PrintWriter(socket.getOutputStream());
            out.println(msg);
            out.flush();
            BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line=in.readLine();
            System.out.println(line);
            //关闭资源
            in.close();
            out.close();
            socket.close();
            System.out.println("client关闭"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(new Date()));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }
}

NioSocket的使用

nio(new IO)是JDK1.4新增长的IO模式,nio在底层采用了与新的处理方式大大的提升了Java IO的效率。Socket也属于IO的一种,nio提供了相对应的类:ServerSocketChannel和SocketChannel,分别对应原来的ServerSocket和Socket。socket

理解nio三基础:

1. Buffer

2. Channel

3. Selector

小故事里有大智慧:

试想一下若是电商是只要有订单就派人直接取货去送货【这种模式就至关于以前的Socket方式】,而不是如今的快递模式:将货物统一到中转站,分拣员按照配送范围分配快递员,而后快递员统一送货【这种模式就至关于NioSocket模式】。那么你得多长时间收到你得快递/(ㄒoㄒ)/~~
Buffer就是货物,Channel就是快递员,Selector就是中转站的分拣员。this

NioSocket使用步骤:

1. 建立ServerSocketChannel并设置相应参数

SerSocketChannel可使用自身的静态工厂方法open建立。
每一个ServerSocketChannel对应一个ServerSocket,能够调用其socket方法来获取【不过若是使用该ServerSocket监听请求就又回到原来的 普通Socket 模式了,通常只用于使用bind方法绑定端口】。
ServerSocketChannel能够经过`SelectableChannel configureBlocking(boolean block)` 方法来设置是否采用阻塞模式。设置非阻塞模式以后能够调用register方法注册Selector【阻塞模式不可使用Selector】

2. 建立Selector并注册Selector到ServerSocketChannel上

Selector可使用自身的静态工厂方法open建立。
建立后经过上面所说的Channel的register方法注册到ServerSocketChannel或者SocketChannel上。

3.调用Selector的select方法等待请求

经过select方法等待请求,select方法可传入表明最长等待时间的long型参数。在设定时间内接收到相应操做的请求则返回能够处理请求的数量,不然在超时后返回0,程序继续执行。若是传入0或者使用无参的重载方法,则会采用阻塞模式直到有相应操做的请求出现。

4. 使用Selector接收请求并处理

接收到请求后Selector调用selectedKeys返回SelectionKey的Set集合。

5. 使用SelectionKey获取到Channel、Selector和操做类型并进行具体操做。

SelectionKey保存了处理当前请求的Channel和Selector,并提供了不一样的操做类型。前面提到的Channel注册Selector的register方法参数中第二个参数就是SelectionKey定义的。共有四种:
SelectionKey.OP_ACCEPT   //请求操做
SelectionKey.OP_CONNECT  //连接操做
SelectionKey.OP_READ     //读操做
SelectionKey.OP_WRITE    //写操做

只有在register方法中注册了对应的操做Selector才会关心相应类型操做的请求。.net

Selector和Channel是多对多关系。
Selector是按不一样的操做类型进行分拣,将分拣结果保存在SelectionKey中,可分别经过SelectionKey的channel、selector方法来获取对应的Channel和Selector。可使用SelectionKey的isAcceptable、isConnectable、isReadable和isWritable方法来判断是什么类型的操做。

Buffer是专门用于存储数据,有四个极为重要的属性:

  1. capacity:容量。
    Buffer最多能够保存元素的数量,建立时设置,使用过程当中不可修改。线程

  2. limit:可使用的上限。
    刚建立Buffer时limit等于capacity。若是给limit设置【不能超过capacity】以后,limit就成了最大可访问的值。code

例如,一个Buffer的capacity为100,表示最多能够保存100个数据,只写入20个以后就要读取,在读取时limit就会设置为20。orm

  1. position:当前所操做元素所在索引位置。
    position从0开始,随着get和put方法自动更新。server

  2. mark:用来暂时保存position的值。
    position保存到mark以后就能够修改并进行相关的操做,操做完成后能够经过reset方法将mark的值恢复到position。

mark默认值为-1,且其值必须小于position的值。
例如,Buffer中一共保存了20个数据,position为10,如今想读取15到20之间的数据,这时就能够调用Buffer的mark方法将目前的position保存到mark中,而后调用Buffer的position(15)将position指向第15个元素,这时就能够读取。读取完成以后使用Buffer的reset就能够将position恢复到10.
若是调用Buffer的position方法时传入的值小于mark当前的值,则会将mark设为-1。
这四个属性大小关系:mark<=position<=limit<=capacity

咱们将前面的普通Socket示例的服务端改写一下:

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;
import java.util.Iterator;

/**
 * @author sanchan
 * @since 1.0
 */
public class NIOServer {
    public static void main(String[] args) {
        /**
         * 启动监听
         * 当监听到请求时根据SelectionKey的操做类型交给内部类Handler进行处理
         */
        try {
            //建立ServerSocketChannel
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //设置监听8080端口
            ssc.socket().bind(new InetSocketAddress(8080));
            //设置为非阻塞模式
            ssc.configureBlocking(false);
            //为ServerSocketChannel注册Selector
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            //建立Handler
            Handler handler = new Handler(1024);
            while (true) {
                //等待请求,每次等待阻塞3s,超过3秒后线程继续运行,若是传入0或使用无参重载方法,将一直阻塞
                if (selector.select(3000) == 0) {
                    System.out.println("等待请求超时~~~~~");
                    continue;
                }
                System.out.println("处理请求~~~~~");
                //获取等待处理的SelectionKey
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    try {
                        //根据不一样请求操做选择对应的处理方法
                        if (key.isAcceptable()) {
                            handler.handleAccept(key);
                        }
                        if (key.isReadable()) {
                            handler.handleRead(key);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        //若是异常就说明链接结束,移除
                        keyIterator.remove();
                        continue;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }

    }

    /**
     * 请求处理类
     */
    private static class Handler {
        private int bufferSize = 1024;
        private String localCharset = "UTF-8";

        public Handler() {
        }

        public Handler(int bufferSize) {
            this(bufferSize, null);
        }

        public Handler(String localCharset) {
            this(-1, localCharset);
        }

        public Handler(int bufferSize, String localCharset) {
            if (bufferSize > 0)
                this.bufferSize = bufferSize;
            if (localCharset != null)
                this.localCharset = localCharset;
        }

        /**
         * 处理请求操做
         *
         * @param key
         * @throws IOException
         */
        public void handleAccept(SelectionKey key) throws IOException {
            //获取Channel
            SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
            //设置非阻塞
            sc.configureBlocking(false);
            //注册读操做的Selector
            sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        /**
         * 处理读操做
         *
         * @param key
         * @throws IOException
         */
        public void handleRead(SelectionKey key) throws IOException {
            //获取Channel
            SocketChannel sc = ((SocketChannel) key.channel());
            //获取ByteBuffer
            /**
             * Buffer专门用于存储数据,有四个极为重要的属性:
             * 1. capacity:容量。
             *  Buffer最多能够保存元素的数量,建立时设置,使用过程当中不可修改。
             * 2. limit:可使用的上限。
             *  刚建立Buffer时limit等于capacity。若是给limit设置【不能超过capacity】以后,limit就成了最大可访问的值。
             *  例如,一个Buffer的capacity为100,表示最多能够保存100个数据,只写入20个以后就要读取,在读取时limit就会设置为20。
             * 3. position:当前所操做元素所在索引位置。
             *  position从0开始,随着get和put方法自动更新。
             * 4. mark:用来暂时保存position的值。
             *  position保存到mark以后就能够修改并进行相关的操做,操做完成后能够经过reset方法将mark的值恢复到position。
             *  mark默认值为-1,且其值必须小于position的值。
             *  例如,Buffer中一共保存了20个数据,position为10,如今想读取15到20之间的数据,这时就能够调用Buffer的mark方法将目前的position保存到mark中,而后调用Buffer的position(15)将position指向第15个元素,这时就能够读取。读取完成以后使用Buffer的reset就能够将position恢复到10.
             *  若是调用Buffer的position方法时传入的值小于mark当前的值,则会将mark设为-1。
             */
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            //重置ByteBuffer。设置limit=capacity、position=0、mark=-1
            buffer.clear();
            //没有获取到内容则关闭
            if (sc.read(buffer) == -1) {
                sc.close();
            } else {
                /**
                 * flip()做用:
                 * 在保存数据时保存一个数据position加1,保存完成后要读取数据
                 * 就得设置limit=position,position=0
                 **/
                buffer.flip();
                //返回数据到客户端
                String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                System.out.println("从客户端获取到了数据:" + receivedString);
                String sendString = "服务端已经获取到了数据:" + receivedString;
                buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
                sc.write(buffer);
                //关闭SocketChannel
                sc.close();
            }

        }
    }
}

我是广告

本人的直播课程在 7 月份就要开始了,但愿小伙伴们支持一下,如今报名有优惠噢

https://segmentfault.com/l/15...

https://segmentfault.com/l/15...

相关文章
相关标签/搜索