Java网络编程之Socket

Java网络编程是什么?

通俗来讲网络编程就是计算机借助网络进行信息传递,通过编码进行消息数据的发送和接收处理。根据个人理解,java中的网络编程,主要是指通过Socket来实现客户端与服务器之间的数据发送和接收。

What is Socket?

Socket也就是套接字 ,官方说法是这样的:套接字是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。它是网络编程的主角,是服务端和客户端建立连接进行通信的媒介。

Socket分类

Socket主要分为三类:流套接字,数据报套接字以及原始套接字

流套接字(SOCK_STREAM)

使用传输协议为TCP协议,是面向连接的相对较稳定的协议,连接建立时存在三次握手和四次分手的过程。

数据包套接字(SOCK_DGRAM)

使用传输协议为UDP协议,是无连接的不稳定协议。传输数据时,不需要建立连接,直接发送数据报,时效性强,不关心发送结果,因此不稳定,需要程序确定数据报是否丢失及补发。

原始套接字(SOCK_ROW)

流套接字和数据包套接字都是基于传输层协议TCP或者UDP进行通信,根据TCP/IP五层协议模型,应用层数据由传输层添加端口信息(TCP首部或UDP首部),进而由网络层添加IP首部补充IP信息。所以在传输层协议层面无法进行IP首部信息的修改,当出现修改或伪造IP需求时无法满足。这时就可以使用原始套接字进行IP的设置修改,因为原始套接字直接工作在数据链路层,可对传输层的TCP/UDP首部及IP首部进行自定义。

流套接字与数据包套接字区别理解

通常我们说的网络编程默认是指使用tcp协议流式套接字编程,可以将流式套接字理解电话聊天,打电话会存在明显的拨号状态,也就是Socket建立连接的过程。两个好友打电话正式交流时,会存在两个通话窗口,你与朋友的通话界面以及朋友与你的通话窗口,同样流式套接字也会成对出现,客户端套接字用于标识所连接服务端的信息,服务端也会有相应Socket标识对应连接的客户端。数据包套接字可以理解为短信发送,打开信息应用,设置收信人,编辑短信内容直接发送,从状态上来看不存在明显的连接过程。

Java中的相关Socket类

Socket

Socket位于java.net包内,是套接字Java中的主要操作类。通过Socket对象进行连接的建立和消息发送接收。

常用构造方法

Socket(InetAddress address, int port)
创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号,这里的host可以是域名也可以是ip。
Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
创建一个套接字并将其连接到指定远程地址上的指定远程端口,当存在多个网关时可指定本机ip和端口号。
Socket(String host, int port, InetAddress localAddr, int localPort)
创建一个套接字并将其连接到指定远程主机上的指定远程端口,当存在多个网关时指定本机ip和端口号。
当不指定本机ip和端口号时,系统会绑定一个临时端口和一个有效的本地地址。

关键方法

bind(SocketAddress bindpoint)
绑定本地地址及端口,进行socket连接时是可选项,不设置时会使用临时端口和默认本地地址。
getInputStream()
与远程主机建立连接后,获得输入流,读取远程输入信息,输入流获取后可单独关闭。
getOutputStream()
与远程主机建立连接后,获取输出流,进行信息写入,信息写入完成后可单独关闭。
close()
关闭Socket连接。

InetAddress

InetAddress用于标识网络上的硬件资源,表示互联网协议(ip)地址,是java对ip地址的封装。其实例对象包含以数字形式保存的IP地址,主机名。InetAddress类提供了将主机名与ip相互转换的方法,还提供了获取本机ip和主机名的方法。InteAddress不对外开放构造方法,可通过静态方法构造InteAddress对象。InetAddress有两个直接子类:Inet4Address和Inet6Address,分别表示Ipv4地址和ipv6地址。

关键方法

方法

说明

static InetAddress[] getAllByName(String host)

获取主机解析的所有地址InetAddress数组

static InetAddress getByName(String host)

返回给定主机的首个InetAddress对象

static InetAddress getByAddress(byte[] addr)

返回给定ip地址的InteAddress,ipv4为4个字节,ipv6为16个字节

static InetAddress getByAddress(String host, byte[] addr)

返回给定主机和ip的InetAddress对象

static InetAddress getLocalhost()

返回本机InetAddress

ServerSocket

ServerSocket顾名思义,用于服务端建立socket连接的对象,通过ServerSocket指定端口等待客户端发起请求,收到请求后ServerSocket会返回对应的Socket对象,从而使用Socket进行后续通信。

常用构造方法

ServerSocket(int port)
指定绑定端口构造ServerSocket实例
ServerSocket(int port, int backlog)
指定绑定端口并设置等待队列中最大的请求数,backlog默认值为50。当请求完成三次握手未被及时处理会尝试放入等待队列中,若当队列中的请求数达到backlog设置的最大值,则当前请求会被拒绝,否则会放入队列中等待accept()方法调用进行处理。
ServerSocket()
无参构造函数,允许服务端在绑定特定端口前配置相关选项,需要手动调用bind()方法绑定端口。
ServerSocket(int port, int backlog, InetAddress bindAddr)
指定ip 、端口、及等待队列最大请求数,当本机只有一个ip时为默认ip,当存在多个ip时可指定ip进行绑定。

关键方法

void bind(SocketAddress endpoint)
绑定地址,SocketAddress为抽象类,直接子类为InetSocketAddress,而InetSocketAddress包含了InetAddress,在此基础上增加了端口port,专门用于建立端对端的连接,调用无参构造器后可使用此方法进行通信ip端口设置。
void bind(SocketAddress endpoint, int backlog)
绑定端口并设置队列最大排队请求数,调用无参构造后可调用此方法。
Scoket accept()
堵塞当前线程,获取新请求,只有有新请求进入后返回对应Socket对象,用于后续通信。
void shutdownOutput()
关闭输出,数据写入完成后调用,会在数据结尾处写入-1,标识数据写入完毕。方法调用后再进行数据写入会抛出IO异常。
void shutdownInput()
关闭输入,表示不再接收数据输入。方法调用后InputStream.available()会返回0,read方法会返回-1。
void close()
关闭ServerSocket端口监听,不在处理新请求。

DatagramSocket

数据包通信套接字,用于进行基于UDP协议的通信。通过发送和接收数据包(DatagramPacket)的方式进行数据传输。发送数据的目标地址由DatagramPacket确定,就像快递站和快递,快递站不知道某个快递的收货地址,由快递本身确定目的地。

构造方法

DatagramSocket()
构造DatagramSocket实例,绑定本机默认ip,端口随机
DatagramSocket(int port)
构造DatagramSocket实例,绑定本机默认ip,绑定端口port
DatagramSocket(int port, InetAddress laddr)
构造DatagramSocket实例,当主机有多个ip时绑定指定ip及端口

关键方法

bind(SocketAddress address)
绑定本机Socket地址包含ip和端口,与Socket类似,在使用无参构造器后调用。
void receive(DatagramPacket p)
阻塞接收数据包。
void send(DatagramPacket p)
发送数据包。
void connect(InetAddress address, int port)
这个方法就很神奇,明明讲了DatagramPacket是使用UDP协议的,无连接的,为什么会提供连接方法呢,这不是打自己脸么。仔细看了下文档:

/**
     * Connects the socket to a remote address for this socket. When a
     * socket is connected to a remote address, packets may only be
     * sent to or received from that address. By default a datagram
     * socket is not connected.
     *
     * <p>If the remote destination to which the socket is connected does not
     * exist, or is otherwise unreachable, and if an ICMP destination unreachable
     * packet has been received for that address, then a subsequent call to
     * send or receive may throw a PortUnreachableException. Note, there is no
     * guarantee that the exception will be thrown.
     *
     * <p> If a security manager has been installed then it is invoked to check
     * access to the remote address. Specifically, if the given {@code address}
     * is a {@link InetAddress#isMulticastAddress multicast address},
     * the security manager's {@link
     * java.lang.SecurityManager#checkMulticast(InetAddress)
     * checkMulticast} method is invoked with the given {@code address}.
     * Otherwise, the security manager's {@link
     * java.lang.SecurityManager#checkConnect(String,int) checkConnect}
     * and {@link java.lang.SecurityManager#checkAccept checkAccept} methods
     * are invoked, with the given {@code address} and {@code port}, to
     * verify that datagrams are permitted to be sent and received
     * respectively.
     *
     * <p> When a socket is connected, {@link #receive receive} and
     * {@link #send send} <b>will not perform any security checks</b>
     * on incoming and outgoing packets, other than matching the packet's
     * and the socket's address and port. On a send operation, if the
     * packet's address is set and the packet's address and the socket's
     * address do not match, an {@code IllegalArgumentException} will be
     * thrown. A socket connected to a multicast address may only be used
     * to send packets.
     *
     * @param address the remote address for the socket
     *
     * @param port the remote port for the socket.
     *
     * @throws IllegalArgumentException
     *         if the address is null, or the port is out of range.
     *
     * @throws SecurityException
     *         if a security manager has been installed and it does
     *         not permit access to the given remote address
     *
     * @see #disconnect
     */
    public void connect(InetAddress address, int port) {
        try {
            connectInternal(address, port);
        } catch (SocketException se) {
            throw new Error("connect failed", se);
        }
    }

简单翻译一下:本方法是用于连接远程的Socket地址(ip和端口),默认是不连接的,当连接之后,数据包的发送和接收的目标地址和来源只能是已连接的socket地址。当远程Socket地址不存在或者不可用时可能会抛出异常,注意这里是可能。如果安全管理器(SecurityManager)被设置了的话,会检查是否能与远程地址通信。具体来说就是,当远程地址是广播地址时,会调用SecurityManager.checkMulticast()对远程地址进行广播检测,否则会检查远程地址是否可以连接并接收数据。当socket调用connect()方法进行连接后,后续发送和接收数据包不用再进行安全检查,否则每次发送数据包都会调用前面的检测方法进行检测。套接字连接远程广播地址通常只是用来发送数据包,可以避免多次发送数据包时重复检测,提高效率。
void disconnect()
断开与远程地址的连接。

DatagramPacket

DatagramPacket是进行数据包套接字通信,发送和接收数据的容器,包含了完整的消息内容以及消息的目标地址信息。

构造方法

DatagramPacket(byte buf[], int offset, int length, SocketAddress address)
构造数据包实例,包含要发送的字节数组,数据在数组中的起始位置、长度以及目标套接字地址,其他构造方法都与此方法类似,不再赘述。

Socket通信过程

根据socket类型的不同,通过socket进行通信的过程也有所不同,其中最显著的区别在于流套接字存在一个连接建立与关闭的过程,而数据包套接字无需建立连接。

流套接字通信

流套接字为确保可靠性会发送数据之前进行连接的建立,数据传输结束后释放连接。建立流套接字连接及通信过程如下:

  1. 服务端初始化ServerSocket对象,绑定host及端口,进行客户端连接监听
  2. 当客户端需要连接服务器时,需要创建一个Socket对象,指定Socket连接的ip和端口,进行连接。
  3. ServerSocket获取到新连接,也会生成一个Socket对象与当前客户端的连接,
  4. 客户端发送消息,服务端接收消息并处理
  5. 服务端回执处理结果,客户端收到处理结果
  6. 客户端关闭Socket,断开连接,服务端关闭Socket,断开当前连接。注意Socket连接是单向的,客户端和服务端都可以自行断开。
    整个过程可以由下图表示:

数据包套接字通信

数据包套接字不用建立连接,各自构建客户端(DatagramSocket)后,可直接进行消息发送和接收,不用获取客户端与服务端对应的一对Socket对象,只用获取相应消息数据的数据包(DatagramPacket)。

过程比较简单,如下图所示:

java socket加密通讯 通讯 java socket协议_网络


由于数据包套接字不存在建立连接的状态,因此接收端收到数据包后无法直接给出消息响应,需要用过再次发送消息的方式,进行消息回执。udp不稳定,出现网络抖动等异常时,会有丢包的风险,因此对数据要求较高的情景需要添加数据校验,要求发送端进行补发或者重发。

代码示例

流套接字通信

public class StreamSocketPractice {
    static ExecutorService serviceExecutorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
    static ExecutorService clientExecutorService = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

    public static void main(String[] args) throws IOException {
        final ServerSocket serverSocket = new ServerSocket(8091, 100);
        serviceExecutorService.execute(
                new Runnable() {
                    public void run() {
                        try {
                            Socket socket;
                            while ((socket = serverSocket.accept()) != null) {
                                InputStream in = socket.getInputStream();
                                DataInputStream dataInputStream = new DataInputStream(in);
                                System.out.println("收到客户端消息:" + socket.getRemoteSocketAddress() + " " + dataInputStream.readUTF());
                                //关闭输入流时,需要调用socket.shutdownInput(),不能调用in.close(),in.close()与socket.close()一样,
                                // 会直接关闭socket
                                socket.shutdownInput();
                                DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
                                dataOutputStream.writeUTF("hello socket client" + socket.getRemoteSocketAddress());
                                //同理关闭输出
                                socket.shutdownOutput();
                                //关闭当前套接字
                                socket.close();
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
        for (int i = 0; i < 10; i++) {
            final int clientIndex = i;
            clientExecutorService.execute(new Runnable() {
                public void run() {
                    try {
                        //声明连接本地8091端口
                        Socket socket = new Socket("localhost", 8091);
                        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
                        dataOutputStream.writeUTF("hello socket server ,I`m client " + clientIndex);
                        //关闭输出
                        socket.shutdownOutput();
                        InputStream inputStream = socket.getInputStream();
                        DataInputStream dataInputStream = new DataInputStream(inputStream);
                        System.out.println("收到回复:" + dataInputStream.readUTF());
                        //关闭输入
                        socket.shutdownInput();
                        //关闭套接字
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

运行结果

收到客户端消息:/127.0.0.1:60682 hello socket server ,I`m client 3
收到回复:hello socket client/127.0.0.1:60682
收到客户端消息:/127.0.0.1:60683 hello socket server ,I`m client 0
收到回复:hello socket client/127.0.0.1:60683
收到客户端消息:/127.0.0.1:60684 hello socket server ,I`m client 1
收到回复:hello socket client/127.0.0.1:60684
收到客户端消息:/127.0.0.1:60685 hello socket server ,I`m client 4
收到回复:hello socket client/127.0.0.1:60685
收到客户端消息:/127.0.0.1:60686 hello socket server ,I`m client 2
收到回复:hello socket client/127.0.0.1:60686
收到客户端消息:/127.0.0.1:60687 hello socket server ,I`m client 5
收到回复:hello socket client/127.0.0.1:60687
收到客户端消息:/127.0.0.1:60688 hello socket server ,I`m client 6
收到回复:hello socket client/127.0.0.1:60688
收到客户端消息:/127.0.0.1:60689 hello socket server ,I`m client 7
收到回复:hello socket client/127.0.0.1:60689
收到客户端消息:/127.0.0.1:60690 hello socket server ,I`m client 8
收到回复:hello socket client/127.0.0.1:60690
收到客户端消息:/127.0.0.1:60691 hello socket server ,I`m client 9
收到回复:hello socket client/127.0.0.1:60691

注意:socket进行InputStream流读取时不能使用这种简单的方式:

InputStream in = socket.getInputStream();
		//in.available()此时很有可能读取为0或者不是全部数据,
		//因为缓冲区尚未准备好或者客户端数据尚未发送完成。
         byte[] inputByteArray = new byte[in.available()];
         //此时字节数组会出现为空或者不完整的情况
		 in.read(inputByteArray);
         System.out.println("收到消息:" + new String(inputByteArray));

异常结果:

收到消息:
收到回复:
收到回复:
收到回复:
收到回复:
收到回复:
收到消息:hello server I`m client 4
收到消息:hello server I`m client 3
收到消息:hello server I`m client 2
收到回复:
收到回复:
收到回复:
收到消息:hello server I`m client 0
收到回复:
收到回复:
收到消息:hello server I`m client 6
收到消息:hello server I`m client 8
收到消息:hello server I`m client 5
收到消息:hello server I`m client 9
收到消息:hello server I`m client 7

由于流式套接字数据传输是流式的,数据是边读边写的,因此在读取数据时,数据很有可能尚未传输完成。因此真实传输数据时大部分会将数据总长度放在消息头部,服务端在收到消息后先获取到数据长度,然后判断收到的数据是否达到总长,从而确保数据读取完成。当传输基本类型或者String字符长度不超过65535时我们可以使用DataInputStream和DataOutputStream进行读写,使用DataOutputStream.writeUTF()写入String时,会将数据长度放在字节流前两位用于确认数据传输完成,同样使用使用DataInputStream.readUTF()读取时会截取前两个字节用于判断数据接收完毕。当字符数超过65535,比如文件传输时就需要我们自己去添加数据长度头了。

数据包通信

/**
 * @author Mr.m
 * 2020/10/14
 */
public class DatagramSocketPractice {
    static Executor serverExecutor= Executors.newSingleThreadExecutor();
    static Executor clientExecutor= Executors.newFixedThreadPool(10);
    public static void main(String[] args) {
        serverExecutor.execute(() -> {
            try {
                DatagramSocket datagramSocket=new DatagramSocket(8090);
                byte[] buf=new byte[1024];
                DatagramPacket datagramPacket=new DatagramPacket(buf,buf.length);
                datagramSocket.receive(datagramPacket);
                System.out.print("收到: "+datagramPacket.getSocketAddress()+" 消息 "+new String(datagramPacket.getData(),0,datagramPacket.getLength()));

            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        clientExecutor.execute(() -> {
            try{
                DatagramSocket datagramSocket=new DatagramSocket(8091,InetAddress.getByName("localhost"));
                byte[] buf="hello I'm client\n".getBytes();
                DatagramPacket datagramPacket=new DatagramPacket(buf,0,buf.length,InetAddress.getLocalHost(),8090);
                datagramSocket.send(datagramPacket);
            }catch (IOException e){
                e.printStackTrace();
            }
        });
    }
}

运行结果如下:

收到: /127.0.0.1:8091 消息 hello I'm client