Java Socket 入门

Java Socket 入门JavaSocket入门1.Socket简介1.1关键问题一:如何定位到目标应用1.2关键问题二:与TCP/IP关系1.3关键问题三:socket工作过程2.TCPEchoRequest样例2.1Server端2.2Client端3.UDPEchoRequest实例3.1Server端3.2Client端4.UDPSocket与TCPS…

1. Socket 简介

socket,即套接字,是一种介于应用和传输层之间的抽象层,可实现同网络内不同应用之间相互发送和接收数据。

对于 socket,有以下三个关键问题:

1.1 关键问题一:如何定位到目标应用

在发送和接收数据时,关键在于如何定位到应用,而定位到应用首先需要定位到主机。在同一子网内,每台主机的 IP 地址是唯一的,故可以借助 IP 地址定位主机,而在每一台主机上,不同的程序往往监听在不同的端口号上,因此,可以利用端口号来定位应用。
因此,一个 socket 由一个 url 地址和一个 port 端口号唯一确定。

1.2 关键问题二:与 TCP/IP 关系

socket 与 TCP/IP 协议簇无关,其本质是编程接口,用于向应用提供数据传输服务的接口。Java 中的 socket 主要是基于 TCP/IP 的封装,在使用过程中,
应用可借助socket接口,建立基于TCP或UDP的数据传输机制,从而实现同网络跨应用之间的数据传输功能,从而将应用与传输层的具体协议分离开来,使得上层应用无需关注过多细节,只专注数据传输即可。
应用之间数据传输过程

1.3 关键问题三:socket 工作过程

socket、协议与端口的关系

  1. 根据传输类型选择 socket 接口进行调用,以 TCP/IP 协议簇为例,主要包括两种socket,即针对 TCP 的流 socket 和针对 UDP 的数据报 socket。
  2. 根据目的应用主机、所在端口号等信息实现寻址过程。
  3. 链接建立与数据传输。
  4. 关闭链接。

看到一张比较详细的socket过程,图片转自 link.
socket工作过程

2. TCP Echo Request 样例

2.1 Server 端

运行参数设置:
在这里插入图片描述

服务端代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;

public class TCPEchoServer { 
   
    private static final int BUFSIZE = 32;

    public static void main(String[] args) throws IOException { 
   
        // 参数校验,传入参数 端口号
        if (args.length != 1) { 
   
            throw new IllegalArgumentException("Parameter(s): <Port>");
        }

        int servPort = Integer.parseInt(args[0]);

        // 创建 ServerSocket 实例,并监听给定端口号 servPort
        ServerSocket servSock = new ServerSocket(servPort);
        int recvMsgSize;
        byte[] receiveBuf = new byte[BUFSIZE];

        while (true) { 
   
            // 用于获取下一个客户端连接,根据连接创建 Socket 实例
            Socket clntSock = servSock.accept();
            // 获取客户端地址和端口号
            SocketAddress clientAddress = clntSock.getRemoteSocketAddress();
            System.out.println("Handling client at " + clientAddress);

            // 获取 socket 的输入输出流
            InputStream in = clntSock.getInputStream();
            OutputStream out = clntSock.getOutputStream();

            // 每次从输入流中读取数据并写到输出流中,直至输入流为空
            while ((recvMsgSize = in.read(receiveBuf)) != -1) { 
   
                out.write(receiveBuf, 0, recvMsgSize);
            }

            // 关闭 Socket
            clntSock.close();
        }
    }
}

必要说明:

  1. Socket 中的输入输出流是流抽象,可看做一个字符序列,输入流支持读取字节,输出流支持取出字节。每个 Socket 实例都维护了 一个 InputStream 和一个 OutputStream 实例,数据传输也主要依靠从流中获取数据并解析的过程。
  2. ServerSocket 与 Socket 区别,ServerSocket 主要用于服务端,用于为新的 TCP 连接请求提供一个新的已连接的 Socket 实例。Socket 则用于服务端和客户端,用于表示 TCP 连接的一端。因此,服务端需要同时处理 ServerSocket 和 Socket 实例,而客户端只需要处理 Socket 实例即可。

2.2 Client 端

运行参数设置:
在这里插入图片描述

客户端代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;

public class TCPEchoClient { 
   
    public static void main(String[] args) throws IOException { 
   
        // 参数校验,传入格式 url "info" 或 url "info" 10240
        if ((args.length < 2) || (args.length > 3)) { 
   
            throw new IllegalArgumentException("Parameter(s): <server><word> [<Port>]");
        }

        // 获取目标应用 url
        String server = args[0];
        // 将传送数据转化为字节数组
        byte[] data = args[1].getBytes();
        // 解析端口号,若无则设为 10240
        int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 10240;

        // 根据参数创建 Socket 实例
        Socket socket = new Socket(server, servPort);

        System.out.println("Connected to server... sending echo string");

        // 获取 socket 的输入输出流
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        // 将数据写入到 Socket 的输出流中,并发送数据
        out.write(data);

        int totalBytesRcvd = 0;
        int bytesRcvd;

        // 接收返回信息
        while (totalBytesRcvd < data.length) { 
   
            if ((bytesRcvd = in.read(data, totalBytesRcvd, data.length - totalBytesRcvd)) == -1) { 
   
                throw new SocketException("Connection closed permaturely");
            }
            totalBytesRcvd += bytesRcvd;
        }

        System.out.println("Received: " + new String(data));

        // 关闭 Socket
        socket.close();
    }
}

必要说明:
发送数据时只通过 write() 方法,接收时为何需要多个 read() 方法?
TCP 协议无法确定在 read() 和 write() 方法中所发送信息的界限,而且发送过程中可能存在乱序现象,即分割成多个部分,所以无法通过一次 read() 获取到全部数据信息。

3. UDP Echo Request 实例

3.1 Server 端

运行参数设置:
在这里插入图片描述

服务端代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UDPEchoServer { 
   
    private static final int ECHOMAX = 255;

    public static void main(String[] args) throws IOException { 
   
        // 参数校验,格式 port
        if (args.length != 1) { 
   
            throw new IllegalArgumentException("Parameter(s): <Port>");
        }

        // 获取端口号
        int servPort = Integer.parseInt(args[0]);
        // 创建数据报文 Socket
        DatagramSocket socket = new DatagramSocket(servPort);
        // 创建数据报文
        DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);

        while (true) { 
   
            // 接收请求报文
            socket.receive(packet);
            System.out.println("Handling client at " + packet.getAddress().getHostAddress() +
                    " on port " + packet.getPort());

            // 发送数据报文
            socket.send(packet);
            // 重置缓存区大小
            packet.setLength(ECHOMAX);
        }
    }
}

必要说明:

  • UDP服务端 与 TCP 服务端不同,TCP 对于每一个客户端请求都需要先建立连接,而 UDP 则不需要。因此,UDP 只需创建一个 Socket 等待客户端连接即可。
  • 典型的 UDP 服务器的常见步骤:
    1. 创建一个 DatagramSocket 实例,指定本地端口号和地址,准备接收报文。
    2. 利用 DatagramSocket 的 receive() 方法接收 DatagramPacket 实例,然后根据接收到的信息,获取客户端地址和端口号。
    3. 使用 DatagramSocket 的 send() 和 receive() 方法来发送和接收 DatagramPackets 实例进行通信。
  • 在该 UDP 服务器的实现中,只接收和发送数据报文中的前 ECHOMAX 个字符,超出部分直接丢弃。
  • 在处理过接收到的消息后,数据包的内部长度会设置为刚处理过的消息长度,通常比初始长度要短,因此需重置缓冲区为初始长度。否则后续可能会使得缓冲区长度不断减小,使得数据包被截断。

3.2 Client 端

运行参数设置:
参数设置

客户端代码:

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.*;

public class UDPEchoClient { 
   
    private static final int TIMEOUT = 3000;
    private static final int MAXTRIES = 5;

    public static void main(String[] args) throws IOException { 
   
        // 参数解析,格式 url "info" 或 url "info" 10240
        if ((args.length < 2) || (args.length > 3)) { 
   
            throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]");
        }

        // 创建目标 Server IP 地址对象
        InetAddress serverAddress = InetAddress.getByName(args[0]);

        // 将需传输字符转换为字节数组
        byte[] byteToSend = args[1].getBytes();
        // 获取服务端端口号,默认 10241
        int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 10241;

        // 创建 UDP 套接字,选择本地可用的地址和可用端口号
        DatagramSocket socket = new DatagramSocket();

        // 设置超时时间,用于控制 receive() 方法调用的实际最短阻塞时间
        socket.setSoTimeout(TIMEOUT);

        // 创建发送数据报文
        DatagramPacket sendPacket = new DatagramPacket(byteToSend, byteToSend.length, serverAddress, servPort);

        // 创建接收数据报文
        DatagramPacket receivePacket = new DatagramPacket(new byte[byteToSend.length], byteToSend.length);

        // 设置最大重试次数,以减少数据丢失产生的影响
        int tries = 0;
        // 是否收到响应
        boolean receivedResponse = false;
        do { 
   
            // 将数据报文传输到指定服务器和端口
            socket.send(sendPacket);
            try { 
   
                // 阻塞等待,直到收到一个数据报文或等待超时,超时会抛出异常
                socket.receive(receivePacket);
                // 校验服务端返回报文的地址和端口号
                if (!receivePacket.getAddress().equals(serverAddress)) { 
   
                    throw new IOException("Received packet from an unknown source");
                }
                receivedResponse = true;
            } catch (InterruptedIOException e) { 
   
                tries += 1;
                System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
            }
        } while (!receivedResponse && (tries < MAXTRIES));

        if (receivedResponse) { 
   
            System.out.println("Received: " + new String(receivePacket.getData()));
        } else { 
   
            System.out.println("No response -- giving up.");
        }
        // 关闭 Socket
        socket.close();
    }
}

必要说明:
由于 UDP 提供的是尽最大可能的交付,所以在发送 Echo Request 请求时,无法保证一定可以送达目标地址和端口,因此考虑设置重传次数,若在超过最大等待时间后仍未收到回复,则重发当前请求,若重发次数超过最大重试次数,则可直接返回未发送成功。

4. UDP Socket 与 TCP Socket 区别

  • UDP 保存了消息的边界信息,而 TCP 则没有。
    在 TCP 中需通过多次 read() 来接收一次 write() 的信息,而 UDP 中对于单次 send() 的数据,最多只需一次 receive() 调用。
  • TCP 存在传输缓冲区,UDP 则无需对数据进行缓存。
    由于 TCP 存在错误重传机制,因此需保留数据的缓存,以便于重传操作,当调用 write() 方法并返回后,数据被复制到传输缓冲区中,数据有可能处于发送过程中或还没有发生传送。而 UDP 则不存在该机制,因此无需缓存数据,当调用 send() 方法返回后,消息处于发送过程中。
  • UDP 会丢掉超过最大长度限制的数据,而 TCP 不会。
    在 TCP 中,一旦建立连接后,对于所有数据都可以看做一个连续的字节序列。而在 UDP 中接收到的消息则可能来自于不同的源地址和端口,因此会将接收到的数据放在消息队列中,按照顺序来响应,超过最大长度的消息直接截断。Datagrampacket 所能传输的最大数据量为 65507 字节,也就是一个 UDP 报文能承载的最大数据。

5. 总结

Socket 的设计与实现,可以看做是对 TCP 和 UDP 在应用层的一种封装方式,对于有连接的 TCP,如何控制连接过程,以准确传输数据对于精确度要求高的程序就比较重要。而对于 UDP,如何提供尽可能便捷、尽可能快的方式就比较重要。
在实际应用中,根据具体需求选择合适的形式即可。


参考文献

[1] Java TCP/IP Socket编程

今天的文章Java Socket 入门分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/29765.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注