JAVA September 30, 2018

Java Socket 基础以及NIO Socket

Words count 21k Reading time 19 mins. Read count 0

1 什么是Socket?

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

关于Socket的介绍:https://blog.csdn.net/httpdrestart/article/details/80670388

2 简单的Socket通讯

下面记录一下,基于java实现Socket通讯,主要包括Socket服务端和Socket客户端两部分。

2.1 基于TCP的Socket服务端

服务端的实现主要有一下几步:

第一步:创建socket服务,并绑定监听端口

第二步:调用accept()方法开始监听,等待客户端的链接

第三步:获取输入流,读取客户端信息

第四步:获取输出流,响应客户端信息

第五步:关闭资源

这里写一个简单的例子,实现服务端读取并客户端发送的消息,然后将客户端发送过来的信息再发回客户端。实现代码如下:

package com.springboot.demo.socket.tcp;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by shirukai on 2018/9/19
 * 基于Tcp的socket服务端
 */
public class TcpSocketServer {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public TcpSocketServer() {
    }

    public TcpSocketServer(int port) {
        try {
            // 1. 创建一个服务器端Socket,指定监听端口
            ServerSocket serverSocket = new ServerSocket(port);
            // 2. 调用accept()方法开始监听,等待客户端的连接
            Socket client = serverSocket.accept();
            // 处理Socket
            handleSocket(client);
        } catch (IOException e) {
            log.error("Create ServerSocket failed:{}", e.getMessage());
            throw new RuntimeException("Create ServerSocket failed:" + e.getMessage());
        }
    }

    public void handleSocket(Socket client) {
        BufferedReader bufferedReader = null;
        BufferedWriter bufferedWriter = null;
        try {
            // 3. 获取输入流,读取客户端信息
            bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            // 4. 获取输出流,响应客户端信息
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
            String messages;
            while ((messages = bufferedReader.readLine()) != null) {
                log.info("Receive message sent by the client:{}", messages);
                bufferedWriter.write("Hi Client,Your message is " + messages + "\n");
                bufferedWriter.flush();
            }

        } catch (IOException e) {
            log.error("The Server accept error:{}", e.getMessage());
        } finally {
            // 5. 关闭资源
            try {
                if (bufferedWriter != null) {
                    bufferedWriter.close();
                }
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
                if (client != null) {
                    client.close();
                }
            } catch (IOException e) {
                log.error("Close source error:{}", e.getMessage());
            }

        }
    }


    public static void main(String[] args) {
        TcpSocketServer socketServer = new TcpSocketServer(9090);
    }
}

2.2 基于TCP的Socket客户端

基于TCP的Socket客户端实现主要有四步:

第一步:创建客户端Socket,指定远程服务器的地址和端口

Socket clientSocket = new Socket(remoteIp,remotePort)

第二步:获取输出流,用来向服务器发送信息

clientSocket.getOutputStream()

第三步:获取输入流,用来获取服务器的响应

clientSocket.getInputStream()

第四步:释放资源

socket.close()

具体实现代码如下所示:

package com.springboot.demo.socket.tcp;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * Created by shirukai on 2018/9/19
 * 基于Tcp 的socket 客户端
 */
public class TcpSocketClient {
    private String remoteIp;
    private int remotePort;
    private Socket socket;
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private BufferedReader bufferedReader;
    private BufferedWriter bufferedWriter;

    public TcpSocketClient(String remoteIp, int port) {
        this.remoteIp = remoteIp;
        this.remotePort = port;
        connectRemote();
    }


    private void connectRemote() {
        try {
            // 1. 创建客户端Socket,指定远程服务器地址和端口
            this.socket = new Socket(remoteIp, remotePort);
            // 2. 获取输出流,向服务器发送信息
            this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            // 3. 获取输入流,获取服务器信息
            this.bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (IOException e) {
            log.error("Connect remote service error:{}", e.getMessage());
            throw new RuntimeException("Connect remote service error");
        }
    }

    public void sendByConsole() {
        Scanner scanner = new Scanner(System.in);
        log.info("Please enter the message: ");
        while (scanner.hasNextLine()) {
            // 读取键盘的输入
            String message = scanner.nextLine();
            sendMessage(message);
        }
        scanner.close();
    }

    public String sendMessage(String message) {
        String result = "";
        try {
            bufferedWriter.write(message + "\n");
            bufferedWriter.flush();
            result = bufferedReader.readLine();
            log.info("From Server:{}", result);
            log.info("Please enter the message: ");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    // 4. 关闭资源
    public void release() {
        try {
            bufferedWriter.close();
            bufferedReader.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {

        TcpSocketClient client = new TcpSocketClient("127.0.0.1", 9090);
        client.sendByConsole();
        // 释放资源
        client.release();
    }
}

效果演示:

2.3 多线程的TCP Socket服务端

上面实现的TCP Socket的服务端,只能处理一个客户端的请求。现在我们要使用多线程,来实现服务端处理多个客户端请求。

package com.springboot.demo.socket.tcp;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

/**
 * Created by shirukai on 2018/9/19
 * 多线程的socket服务端
 */
public class ThreadSocketServer {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public ThreadSocketServer(int port) {
        try {
            // 1. 创建一个服务器端Socket,指定监听端口
            ServerSocket serverSocket = new ServerSocket(port);
            //创建线程池
            ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-call-runner-%d").build();
            int size = 10;
            ExecutorService executorService = new ThreadPoolExecutor(size, size, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), namedThreadFactory);

            while (true) {
                //循环监听
                // 2. 调用accept()方法开始监听,等待客户端的连接
                Socket socket = serverSocket.accept();
                executorService.execute(() -> new TcpSocketServer().handleSocket(socket));
            }
        } catch (IOException e) {
            log.error("Create ServerSocket failed:{}", e.getMessage());
            throw new RuntimeException("Create ServerSocket failed:" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        ThreadSocketServer socketServer = new ThreadSocketServer(9090);
    }
}

2.4 基于UDP的Sokcet服务端

基于UDP的Sokcet服务端实现步骤主要有:

第一步:创建服务器端DatagramSocket,指定端口

第二步:创建数据包,用于接收客户端发送的数据

第三步:接收客户端发来的信息

第四步:创建数据包,用于响应客户端

第五步:响应客户端

第六步:关闭资源

具体代码如下所示:

package com.springboot.demo.socket.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * Created by shirukai on 2018/9/19
 * 基于UDF的Socket服务端
 */
public class UDPSocketServer {
    public static void main(String[] args) throws Exception {
        // 1.创建服务器端DatagramSocket,指定端口
        DatagramSocket server = new DatagramSocket(9090);
        // 2.创建数据包,用于接收客户端发送的数据
        byte[] requestMessage = new byte[1024];
        DatagramPacket requestPacket = new DatagramPacket(requestMessage, requestMessage.length);
        // 3.接收客户端发来的信息
        server.receive(requestPacket);//此方法在接收到数据报之前一直会阻塞
        String info = new String(requestMessage, 0, requestPacket.getLength());
        System.out.println("客户端发来信息:" + info);
        // 4. 创建数据包,用于响应客户端
        InetAddress address = requestPacket.getAddress();
        int port = requestPacket.getPort();
        byte[] responseMessage = "欢迎你".getBytes();
        DatagramPacket responsePacket = new DatagramPacket(responseMessage, responseMessage.length, address, port);
        // 5. 响应客户端
        server.send(responsePacket);
        //关闭资源
        server.close();
    }
}

2.5 基于UDP的Sokcet客户端

基于UDF的Socket客户端的实现主要包括:

第一步:定义服务器的地址、端口号、数据

第二步:创建数据报,包含发送的数据信息

第三步:创建数据报,用于接收服务器响应的数据

第四步:接收服务器响应的数据

第五步:读取数据

第六步:关闭资源

具体实现代码如下所示:

package com.springboot.demo.socket.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * Created by shirukai on 2018/9/19
 * 基于UDP的Socket客户端
 */
public class UDPSocketClient {
    public static void main(String[] args) throws Exception {
        // 1.定义服务器的地址、端口号、数据
        InetAddress address = InetAddress.getByName("localhost");
        int port = 9090;
        DatagramSocket client = new DatagramSocket();
        byte[] sendMessage = "向服务端发送信息".getBytes();
        // 2.创建数据报,包含发送的数据信息
        DatagramPacket sendPacket = new DatagramPacket(sendMessage, sendMessage.length, address, port);
        client.send(sendPacket);
        // 3.创建数据报,用于接收服务器响应的数据
        byte[] receiveMessage = new byte[1024];
        DatagramPacket receivePacket = new DatagramPacket(receiveMessage, receiveMessage.length);
        // 4.接收服务器响应的数据
        client.receive(receivePacket);
        // 5.读取数据
        String reply = new String(receiveMessage, 0, receivePacket.getLength());
        System.out.println(reply);
        // 6.关闭资源
        client.close();
    }
}

3 NIO Socket

上面一节,学习了java基本的Sokcet网络编程,接下来将学习一个新名词,NIO。java.nio全称java non-blocking IO(实际上是 new io),是指jdk1.4 及以上版本里提供的新api(New IO),为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。

关于更多的NIO的资料:

https://www.jianshu.com/p/3cec590a122f

这一节主要记录一下NIO Socket的学习,为什么要用NIO Socket?我们可以通过下面两张图来对比传统的BIO Socket。图片来自https://www.jianshu.com/p/b9f3f6a16911(Netty入门教程——认识Netty)

传统BIO Socket

NIO Sokcet

从上面两张图可以看出,BIO想要通过多线程来实现多连接,因为单个Socket的会发生阻塞。而NIO的单线程的处理能力比BIO强的多,NIO处理socket的不会发送阻塞的现象。这是因为NIO把所有的Socket都交给Selector来处理,只需要不断的遍历Selector里的Socket的然后处理该Socket即可。

下面将分别以具体代码实现NIO Socket的服务端和客户端。

3.1 NIO Socket 服务端

NIO Socket服务端的实现主要有以下几步:

第一步: 创建一个信道并设置信道为非阻塞

ServerSocketChannel channel = ServerSocketChannel.open();
// 设置信道为non-blocking(非阻塞) 默认为blocking
channel.configureBlocking(false);

第二步:从信道中获取SeverSocket并绑定监听端口

ServerSocket socket = channel.socket();
// 获取本机的address
InetSocketAddress address = new InetSocketAddress(9001);
// 将端口绑定到ServerSocket上
socket.bind(address);

第三步: 创建一个Socket选择器

Selector selector = Selector.open();

第四步: 将选择器注册到信道中

// SelectKey四种状态:OP_CONNECT 连接就绪、OP_ACCEPT 接收就绪、OP_READ 读就绪、OP_WRITE 写就绪
channel.register(selector, SelectionKey.OP_ACCEPT);

第五步:搜索信道

selector.select();

第六步: 获取准备好的信道所关联的key几步的iterator实例

Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

第七步:遍历selectedKey,根据key的状态处理Socket

具体代码如下所示:

package com.springboot.demo.netty;

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

/**
 * Created by shirukai on 2018/9/27
 * 非阻塞 I/O socket
 */
public class NioSocketServer {
    public static void main(String[] args) {
        try {
            // 1. 创建一个信道并设置信道为非阻塞
            ServerSocketChannel channel = ServerSocketChannel.open();
            // 设置信道为non-blocking(非阻塞) 默认为blocking
            channel.configureBlocking(false);
            // 2. 从信道中获取ServerSocket并绑定监听端口
            ServerSocket socket = channel.socket();
            // 获取本机的address
            InetSocketAddress address = new InetSocketAddress(9001);
            // 将端口绑定到ServerSocket上
            socket.bind(address);
            // 3. 创建一个Socket选择器
            Selector selector = Selector.open();
            // 4. 将选择器注册到各个信道上
            // SelectKey四种状态:OP_CONNECT 连接就绪、OP_ACCEPT 接收就绪、OP_READ 读就绪、OP_WRITE 写就绪
            channel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                // 5. 搜索信道
                selector.select();
                // 6. 获取准备好的信道所关联的key集合的iterator实例
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                // 7.遍历selectedKey,根据状态处理Socket
                while (keyIterator.hasNext()) {
                    // 获取key值
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();
                    try {
                        if (key.isAcceptable()) {
                            SocketChannel client = ((ServerSocketChannel) key.channel()).accept();
                            client.configureBlocking(false);
                            client.register(key.selector(), SelectionKey.OP_READ);
                            System.out.println("Accepted connection from: " + client);
                        }
                        if (key.isReadable()) {
                            SocketChannel client = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            int readLength = client.read(buffer);
                            //如果有记录
                            if (readLength > 0) {
                                key.interestOps(SelectionKey.OP_READ);
                                String record = new String(buffer.array(), 0, readLength);
                                System.out.println(record);
                                client.write(ByteBuffer.wrap(("服务器已经接受消息:" + record).getBytes()));
                            }
                        }
                        if (key.isWritable()) {

                        }
                    } catch (Exception e) {
                        key.cancel();
                    }
                }
            }

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

3.2 NIO Socket 客户端

NIO Socket客户端的实现主要有以下几步:

第一步:创建一个信道,并绑定远程服务器的IP和端口

SocketChannel channel = SocketChannel.open();
// 设置服务器的address
InetSocketAddress remote = new InetSocketAddress("127.0.0.1", 9001);

第二步: 连接远程服务器

channel.connect(remote);
channel.configureBlocking(false);

第三步: 创建一个Socket选择器

Selector selector = Selector.open();

第四步: 将选择器注册到信道中

channel.register(selector, SelectionKey.OP_READ);

第五步: 启动一个县城用来接收服务器消息

第六步:从键盘获取输入发送给服务器

具体实现代买如下所示:

package com.springboot.demo.netty;

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.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * Created by shirukai on 2018/9/27
 * Nio Socket Client
 */
public class NioSocketClient {
    public static void main(String[] args) {
        try {
            // 1. 创建一个信道,并绑定远程服务器的ip和端口
            SocketChannel channel = SocketChannel.open();
            // 设置服务器的address
            InetSocketAddress remote = new InetSocketAddress("127.0.0.1", 9001);
            // 2. 连接远程服务器
            channel.connect(remote);
            channel.configureBlocking(false);
            // 3. 创建一个Socket选择器
            Selector selector = Selector.open();
            // 4. 将选择器注册到信道中
            channel.register(selector, SelectionKey.OP_READ);
            // 5. 启动一个线程用来接收服务器消息
            new Thread(() -> {
                while (true) {
                    //读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    try {
                        length = channel.read(buffer);
                        if (length > 0) {
                            System.out.println(new String(buffer.array(), 0, length));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            // 6. 从键盘获取输入发送给服务器
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入数据:\n");
            while (scanner.hasNextLine()) {
                System.out.println("输入数据:\n");
                // 读取键盘的输入
                String line = scanner.nextLine();
                // 将键盘的内容输出到SocketChannel中
                channel.write(Charset.forName("UTF-8").encode(line));
            }
            scanner.close();

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

演示效果:

0%