Java基础系列:Socket编程

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

说到前面的话

没有实战案例的理论基础都是在耍流氓,因此今天主要是想经过这里的案例可以让你们加深对以前的理解java

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

Java中的Socket实现

InetAddress

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

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

// 获取本地地址
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了点,生产环境中确定不会这么干oracle

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所在的目录文档:

点击这里查看

相关文章
相关标签/搜索