俗世游子:专注技术研究的程序猿

说到前面的话

没有实战案例的理论基础都是在耍流氓,所以今天主要是想通过这里的案例能够让大家加深对之前的理解

本节我们会一步步实现一个点对点聊天小程序

Java中的Socket实现

InetAddress

InetAddress是Java对IP地址的封装,这个类是一个基础类,下面的ServerSocketDatagramSocket都离不开这个类

InetAddress无法通过new的方式来初始化,只能提供过其提供的静态方法来调用:

// 获取本地地址
InetAddress localHost = InetAddress.getLocalHost();

InetAddress的方法

这里是InetAddress的一些方法:

// 主机名:DESKTOP-ATG4KKE
System.out.println("主机名:" + localHost.getHostName());

// IP地址:192.168.87.1
System.out.println("IP地址:" + localHost.getHostAddress());

// 是否正常:true
System.out.println("是否正常:" + localHost.isReachable(5000));

这里是我测试时的输出,

关于isReachable()的方法,用来检测该地址是否可以访问,由此我们可以做一些健康检查操作,比如:

// 通过主机IP或者域名来得到InetAddress对象
InetAddress inetAddress = InetAddress.getByName("192.168.87.139");
System.out.println("是否正常:" + inetAddress.isReachable(5000));

在5s之内尽最大可能尝试连接到主机,如果没有就认为主机不可用,这里受限于防火墙服务器配置

当然,做健康检查这种方法还是low了点,生产环境中肯定不会这么干

PS: 生产环境的网络操作不会使用到这节里的东西,大部分情况下采用的都是Netty

ServerSocket

ServerSocket是服务端套接字,是基于TCP/IP协议下的实现

初始化

通常我们这样来构建:

ServerSocket serverSocket = new ServerSocket(9999);

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(9999));

这样就完成了服务端的初始化,并且将端口9999绑定起来

等待连接

如果客户端想要和ServerSocket建立连接,我们需要这么做

for(;;) {
    Socket socket = serverSocket.accpet();
    // Socket[addr=/0:0:0:0:0:0:0:1,port=62445,localport=9999]
    System.out.println(socket);
}

accpet()是侦听与ServerSocket建立的连接,这个方法是一个阻塞方法,会一直等待连接接入进来

如果有连接接入进来,我们可以通过返回值来得到当前接入进来的Socket

通信

在网络中传递数据其实也是按照IO流的方式进行传递的,但是我们只能获取到字节流:

InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();

InputStream读取数据,OutputStream写出数据,这些基本操作我们在之前的IO流中都介绍过,这里就不再多说

这里我们为了能够提高效率,可以采用包装流或者处理流来处理,这前面也介绍过了

完整小例子

其实到这里,ServerSocket的关键介绍也就完了,下面我们来做一个小例子:

  • 当有客户端连接进来之后,给客户端返回:Hello World
public class _ServerSocket {
    // 用来存储请求客户端和Socket之间的对应关系
    static Map<String, Socket> MAP = new HashMap<>(); 

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(9999));

            for (; ; ) {
                String token = UUID.randomUUID().toString().replace("-", "").toLowerCase();

                Socket socket = serverSocket.accept();
                // 对应
                MAP.put(token, socket);

                outHtml(socket);
            }

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

    public static void outHtml(Socket socket) {
        OutputStream outputStream = null;
        try {
            outputStream = socket.getOutputStream();
            outputStream.write(("HTTP/1.1 200 OK\n\nHello World").getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

HTTP/1.1 200 OK\n\nHello World\n

这是HTTP协议下返回类型,前面是Response固定格式,Hello World是真正返回的内容,这样我们的ServerSocket就能够通过浏览器来访问了

ServerSocket

Socket

Socket属于客户端套接字,只有先和服务端套接字建立连接才能做其他的操作,Socket的使用方式非常简单

建立连接

Socket socket = new Socket("127.0.0.1", 9999);

// 验证是否连接成功
if (socket.isConnected()) {
    System.out.println("到服务端连接成功");
}

这是其中一种构造方法,更多情况下是采用这种方式

和服务端的连接建立成功之后,后续的操作就和ServerSocket通信步骤一样了,这里就不再多废话了

下面用一个完整的例子来巩固一下

案例:TCP点对点聊天

服务端

public class Server {
    /**
     * 将客户端标识和socket关联起来
     */
    private static final Map<String, Socket> SOCKET_MAP = new HashMap<>();
    /**
     * 反向关联,用来获取标识
     */
    private static final Map<Socket, String> SOCKET_TOKEN_MAP = new HashMap<>();

    public static void main(String[] args) throws IOException {
        /**
         * 开启ServerSocket并监听9999端口
         */
        ServerSocket serverSocket = new ServerSocket(9999);

        for (;;) {
            /**
             * 等待客户端连接
             */
            Socket socket = serverSocket.accept();

            /**
             * IO读取是阻塞式方法,所以需要开启新线程,这里可以优化成线程池
             */
            new Thread(() -> {
                try {
                    saveToMap(socket);
                    getClientMsg(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    /**
     * 绑定SOCKET
     */
    private static void saveToMap(Socket socket) throws IOException {
        String token = StringUtil.uuid();
        SOCKET_MAP.put(token, socket);
        SOCKET_TOKEN_MAP.put(socket, token);

        System.out.println("---客户端连接成功,编号:" + token);
        System.out.println("当前用户:" + SOCKET_MAP.size());

        /**
         * 因为没有登录,所以这里要告知客户端自己的标识
         */
        send(token, token, token);
    }

    /**
     * 获取客户端发送过来的消息,并发送出指定指定的客户端
     */
    private static void getClientMsg(Socket socket) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = "";
        while ((line = reader.readLine()) != null) {
            // 读取到一行以后,从这里发送出去
            send(socket, line);
        }
    }

    /**
     * 发送消息
     */
    private static void send(Socket socket, String line) throws IOException {
        String[] s = line.split("#");
        final String from = SOCKET_TOKEN_MAP.get(socket);
        send(s[0], s[1], from);
    }

    /**
     * 发送消息
     * @param token
     * @param msg
     * @param from 这里在目标客户端展示
     * @throws IOException
     */
    private static void send(String token, String msg, String from) throws IOException {
        Socket sk = SOCKET_MAP.get(token);

        if (null == sk)
            return;

        String s = from + ":" + msg;
        System.out.println("---发送给客户端:" + s );
        // 字符流输出
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(sk.getOutputStream()));
        writer.write(s);
        writer.newLine();
        writer.flush();

    }
}

客户端

public class Client {

    public static void main(String[] args) throws IOException {
        /**
         * 连接到服务端
         */
        Socket socket = new Socket("127.0.0.1", 9999);

        /**
         * 开新线程读取消息,可以优化
         */
        new Thread(() -> {
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String line = "";
                while (StringUtil.isNotBlank(line = reader.readLine())) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        /**
         * 从控制台写入消息并发送出去
         */
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String next = scanner.next();
            send(next, socket);
        }
    }

    private static void send(String msg, Socket socket) throws IOException {
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        writer.write(msg);
        writer.newLine();
        writer.flush();
    }
}

代码已经通过测试,注释写的也非常清楚,大家可以尝试下,按照标识#消息的格式就可以点对点聊天了。

如果想要群聊

  • 将Socket保存到集合中,然后循环集合就可以了,非常简单

好久没有用Socket写聊天程序了,差点就放弃了

下次改用Netty来写,NettySocket方便多了

DatagramSocket

DatagramSocket是用于发送和接收数据报包的套接字,是基于UDP协议下的实现。根据类中官方介绍:

数据报套接字是数据包传递服务的发送或接收点。 在数据报套接字上发送或接收的每个数据包都经过单独寻址和路由。 从一台机器发送到另一台机器的多个数据包可能会以不同的方式路由,并且可能以任何顺序到达

我们也能明白UDP协议的特性。

DatagramPacket

该类表示数据报包,在DatagramSocket中传递和接收数据都是靠这个类来完成的,比如:

  • 接收数据
byte[] buffer = new byte[1024];
DatagramPacket p = new DatagramPacket(buffer, buffer.length);
  • 发送数据
DatagramPacket p = new DatagramPacket("123".getBytes(), "123".getBytes().length, InetAddress.getByName("localhost"), 9999);

发送数据出去,DatagramPacket需要指定接收端的IP和端口,这样才能够发送出去

下面我们来看看具体如何用

初始化

DatagramSocket socket = new DatagramSocket(9999);

DatagramSocket s = new DatagramSocket(null);
s.bind(new InetSocketAddress(9999));

两种方式都可以完成初始化,没有什么区别

接收消息

byte[] buffer = new byte[1024];
DatagramPacket p = new DatagramPacket(buffer, buffer.length);

socket.receive(p);

System.out.println(new String(p.getData(), 0, p.getLength()));

根据DatagramPacket的接收参数,构造出来一个byte[],然后调用receive(),这样消息就接收到了

receive()是一个阻塞方法,只有等有消息的时候才会继续执行

发送消息

DatagramPacket p = new DatagramPacket("123".getBytes(), "123".getBytes().length, InetAddress.getByName("localhost"), 9999);

socket.send(p);

构造发送数据包,然后调用send()方法就可以完成数据包的发送

UDP不需要连接,直接通过IP+PORT的方式就可以发送数据

案例:UDP聊天

public class _DatagramPacket {

    public static void main(String[] args) throws IOException {
        // 从命令行得到需要绑定的端口和发送数据的端口
        DatagramSocket datagramSocket = new DatagramSocket(Integer.parseInt(args[0]));

        System.out.println("已启动");

        new Thread(() -> {

            byte[] buffer = new byte[1024];
            DatagramPacket p = new DatagramPacket(buffer, buffer.length);
            try {
                for (;;) {
                    // 构建接收数据
                    datagramSocket.receive(p);
                    System.out.println(p.getPort() + ":" + new String(buffer, 0, p.getLength()));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        DatagramPacket p = new DatagramPacket(new byte[0], 0, new InetSocketAddress("127.0.0.1", Integer.parseInt(args[1])));
        while (scanner.hasNext()) {
            String next = scanner.next();
            // 构建发送数据包
            p.setData(next.getBytes());
            datagramSocket.send(p);
        }
    }

有瑕疵,空格会换行,这里交给大家去修改了

最后的话

到这里,关于Socket编程方面的东西就聊完了,没有介绍很多的API方法,这些在用到的时候再看也是一样的。

以下是java.net所在的目录文档:

点击这里查看