Detailed introduction to network programming (Socket, TCP communication, Channel communication)

Introductory knowledge

software structure

  • C/S structure : The full name is Client/Server structure, which refers to the client and server structure. Common programs include software such as QQ and Thunder.
  • B/S structure : The full name is Browser/Server structure, which refers to the browser and server structure. Common browsers include Google, Firefox, etc.

Both architectures have their own advantages, but no matter what kind of architecture, they are inseparable from the support of the network.

Network programming : It is a program that realizes the communication between two computers under a certain protocol.

The general flow of network communication is: a data packet is generated by an application program, enters the protocol stack for packaging various headers, and then the operating system calls the network card driver to direct the hardware and send the data to the peer host.

The general diagram of the whole process is as follows:

insert image description here

The protocol stack is actually a stack of some protocols located in the operating system, including TCP, UDP, ARP, ICMP, IP, etc.

Usually a protocol is designed to solve specific problems, such as:

  • The design of TCP is responsible for the safe and reliable transmission of data
  • The design of UDP is that the message is small and the transmission efficiency is high
  • ARP is designed to be able to query physical (Mac) addresses by IP address
  • ICMP is designed to return error messages to hosts
  • The purpose of IP design is to realize the interconnection and intercommunication of large-scale hosts

Common protocols for network communication: UDP|TCP

UDP: A connectionless protocol, the two sides of the communication can send data directly without establishing a connection

  • Advantages: high efficiency, low cost

  • Disadvantages: insecure, easy to lose data


TCP: A connection-oriented protocol, the client and server must go through three handshakes to establish a logical connection before they can communicate

  • Benefits: Safety

  • Disadvantages: low efficiency

Three-way handshake: In the TCP protocol, in the preparation stage of sending data, the client and the server interact three times to ensure the reliability of the connection.

  1. In the first handshake, the client sends a connection request to the server and waits for the server to confirm. // Server are you dead?

  2. In the second handshake, the server sends a response back to the client, notifying the client that the connection request has been received. // I'm alive! !

  3. For the third handshake, the client sends confirmation information to the server again to confirm the connection. // I see! !


TCP/IP protocol

TCP/IP Protocol Reference Model

The TCP/IP protocol reference model classifies all TCP/IP series protocols into four abstraction layers

  • Application layer: TFTP, HTTP, SNMP, FTP, SMTP, DNS, Telnet, etc.
  • Transport layer: TCP, UDP
  • Network layer: IP, ICMP, OSPF, EIGRP, IGMP
  • Data link layer: SLIP, CSLIP, PPP, MTU

Each abstraction layer builds on the services provided by the lower layer and provides services for the higher layer.

insert image description here


IP address

IP address: It is equivalent to the identity number of the computer (unique)

The role of the ip address: it is unique, and another computer can be found through the ip address in the network

IP address classification:

  • ipv4: ip address consists of 4 bytes, one byte is 8 bits (bit 1,0)

    Binary: 11001101.11001100.11000001.11001111

    Decimal: 192.168.0.106

    The range of each byte: 0-255 (2^8), the first bit of the ip address cannot be 0

    Number of ip addresses: 4.2 billion (2^32=4294967296)

    Problem: With the increase of computers, ip addresses are facing exhaustion (global IPv4 addresses were allocated in February 2011), and there are not enough ip addresses, so ipv6 addresses are created

  • ipv6: The ip address consists of 16 bytes, one byte is 8 bits (bit 1,0)

    The number of ip addresses: 2^128=3.4028236692093846346337460743177e+38

    Claims to be able to write an ip address for every grain of sand on earth

    For convenience, use hexadecimal: fe80::a8a6:b83c:8b8b:2685%17

Some common dos commands: dos window win+r ==> cmd ==> dos window

1.查看电脑的IP信息
	命令:ipconfig
    --------------------------------------------------
   	Windows IP 配置
   	连接特定的 DNS 后缀 . . . . . . . :
   	本地链接 IPv6 地址. . . . . . . . : fe80::a8a6:b83c:8b8b:2685%17
   	IPv4 地址 . . . . . . . . . . . . : 192.168.0.106
   	子网掩码  . . . . . . . . . . . . : 255.255.255.0
   	默认网关. . . . . . . . . . . . . : 192.168.0.1
	--------------------------------------------------

2.测试你的电脑和指定ip地址的电脑是否可以连通
	命令:ping ip地址
	--------------------------------------------------
	C:\Users\Administrator>ping 192.168.0.222  没有ping通
    正在 Ping 192.168.0.222 具有 32 字节的数据:
    来自 192.168.0.106 的回复: 无法访问目标主机。
    来自 192.168.0.106 的回复: 无法访问目标主机。
    来自 192.168.0.106 的回复: 无法访问目标主机。
    来自 192.168.0.106 的回复: 无法访问目标主机。
	--------------------------------------------------
    C:\Users\Administrator>ping www.baidu.com
    正在 Ping www.a.shifen.com [61.135.169.121] 具有 32 字节的数据:
    来自 61.135.169.121 的回复: 字节=32 时间=6ms TTL=56
    来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
    来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
    来自 61.135.169.121 的回复: 字节=32 时间=4ms TTL=56
	--------------------------------------------------

3.ping本机的ip地址(你自己电脑的ip地址) 
	命令:ping 127.0.0.1	或	ping localhost 

The port number

The port number is a logical port, which cannot be seen directly, but can be seen by using some software (computer housekeeper, 360.…)

When the network software is opened (for network use), the operating system will assign a random port number to the network software or the port number specified by the operating system when the network software is opened.

The port number is composed of two bytes, representing the range: 2^16=0-65535


Port numbers before 1024, which cannot be used, have been assigned by the operating system to some known network software.

Note: The port numbers of each network software cannot be repeated

Commonly used port numbers:

  • Port 80: network port

  • Database: mysql: 3306, oracle: 1521

  • Tomcat service: 8080


To ensure that the data can be sent to a certain software of the other party's computer accurately, use the ip address: port number

Test whether the port number is connected:telnet ip地址:端口号


InetAddress class: get IP address

java.net.InetAddress : Describes the computer 's ip address

This class represents an Internet Protocol (IP) address.

You can use the method in the InetAddress class to get the ip address of the computer

The way to create an object: static method

static InetAddress getLocalHost() 			// 返回本地主机(你自己电脑的ip地址对象)。
static InetAddress getByName(String host) 	// 在给定主机名的情况下确定主机的 IP 地址。
/* 参数:
 	String host:可以传递主机名称、ip地址、域名
*/

​ Non-static method:

String getHostAddress() 	// 返回 IP 地址字符串(以文本表现形式)。
String getHostName() 		// 获取此 IP 地址的主机名。

Socket: socket

Data generated by applications such as browsers, e-mail, file transfer servers, etc., will be transmitted through transport layer protocols. The application program does not establish a direct connection with the transport layer, but has a suite that can connect the application layer and the transport layer, and this suite is Socket .

insert image description here


Blocking/non-blocking, synchronous/asynchronous

  • Blocking : waiting for the result, nothing can be done

  • non-blocking : can do other things

  • Synchronization : Get results proactively

  • Asynchronous : wait for notification result


  • BIO: Block (blocked) IO [synchronous, blocked]
  • NIO: Non-Block (non-blocking) (synchronous) IO [synchronous, non-blocking] - JDK1.4 started
  • AIO: Asynchronous (asynchronous - non-blocking) IO [asynchronous, non-blocking] - JDK1.7 started

TCP/IP communication

Client of TCP communication: Socket

Function: Actively establish a connection path with the server after 3 handshakes, send data to the server, and read the data written back by the server

The class that represents the client: java.net. Socket : This class implements the client socket (also called "socket").

Socket: A network unit that encapsulates an IP address and port number

Construction method:

public Socket(InetAddress address, int port)	// 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
public Socket(String host, int port) 			// 创建一个流套接字并将其连接到指定主机上的指定端口号。
/* 参数:
   	InetAddress address | String host:传递服务器的ip地址
   	int port:服务器的端口号
*/

​ Member method:

OutputStream getOutputStream()	// 返回此套接字的输出流。
InputStream getInputStream() 	// 返回此套接字的输入流。

void shutdownOutput() 			// 禁用此套接字的输出流。
    							// 对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列

Notice:

  1. When creating a client Socket object, the client will connect the connection path with the server after three handshakes according to the server's IP address and port number

    • The server has started, and the ip address and port number of the server are filled in correctly: the handshake is successful, and the Socket object is created

    • The server is not started, and the ip address and port number of the server are filled incorrectly: the handshake fails, and a connection exception will be thrown

      ConnectException: Connection refused: connect

  2. For data transmission between the client and the server, the stream object created by itself cannot be used (it can only read and write with the local hard disk).

    Use the network stream object provided in the Socket object


The server of TCP communication: ServerSocket

Function: Receive the client's request and establish a connection path with the client after 3 handshakes; read the data sent by the client, and write (send) the data back to the client

A class representing a server: java.net.ServerSocket ; this class implements a server socket.

Construction method:

public ServerSocket(int port) // 创建绑定到特定端口的服务器套接字。

Member method:

Socket accept() 	// 侦听并接受到此套接字的连接。
/* 使用accpet方法,会一直监听客户端的请求
		有客户端请求服务器,accept方法就会获取到请求的客户端Socket对象
		没有客户端请求服务器,accept方法会进入到阻塞状态,一直等待
*/

Notice:

​ When the server started, the following exception was thrown: indicating that the port number used by the server has been occupied, and the port number needs to be changed

​ java.net.BindException: Address already in use: JVM_Bind


File upload case

file upload client

Read the local file, upload it to the server, and read the "upload successful!" written by the server

File upload is a copy of the file:

​ Data source: c:\1.jpg

​ Destination: in the server

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

public class Demo01TCPClient {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //1.创建本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        
        //2.创建客户端Socket对象,构造方法绑定服务器的ip地址和端口号
        Socket socket = new Socket("127.0.0.1", 9999);
        
        //3.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象
        OutputStream os = socket.getOutputStream();
        //4.使用本地字节输入流FileInputStream对象中的方法read,读取要上传的而文件
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes)) != -1){
    
    
            //5.使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器中
            os.write(bytes, 0, len);
        }
        // 上传结束
        socket.shutdownOutput();
        
        //6.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象
        InputStream is = socket.getInputStream();
        //7.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的"上传成功!"
        while ((len = is.read(bytes)) != -1){
    
    
            System.out.println(new String(bytes, 0, len));
        }
        
        //8.释放资源(FileInputStream对象, Socket)
        fis.close();
        socket.close();
    }
}

Server side of file upload (multithreaded)

Read the file uploaded by the client, save the file to the hard disk of the server, and write back "upload successful!" to the client

File upload is a copy of the file:

​ Data source: File 1.jpg uploaded by the client

​ Destination: d:\upload\1.jpg in the hard disk of the server

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Demo02TCPServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //1.判断d盘有没有upload文件夹,没有则创建
        File file = new File("d:\\upload");
        if(!file.exists()){
    
    
            file.mkdir();
        }
        //2.创建服务器ServerSocket对象,和系统要指定的端口号9999
        ServerSocket server = new ServerSocket(9999);
        
        // 一直循环监听客户端的请求(轮询)
        while(true){
    
    
             //3.使用服务器ServerSocket对象中的方法accpet,监听并获取请求的客户端Socket对象
        	Socket socket = server.accept();
            
            // 开启一个新的线程完成这个客户端的文件上传
            new Thread(()->{
    
    
                try {
    
    
                    //4.使用客户端Socket对象中的方法getInputStream,获取网络字节输入流InputStream对象
        			InputStream is = socket.getInputStream();
        			
        			/*
        			    自定义一个文件的名称,防止名称的重复,覆盖之前的文件
        			    规则:不重复 ==> 自己写 ==> 域名 + 毫秒值 + 随机数
        			 */
        			String fileName = "cormorant" + System.currentTimeMillis() 
                        + new Random().nextInt(9999999) + ".jpg";
        			
        			//5.创建本地字节输出流FileOutputStream对象,绑定要输出的目的地
        			//FileOutputStream fos = new FileOutputStream(file + "\\1.jpg");  //d:\\upload\\1.jpg
        			FileOutputStream fos = new FileOutputStream(file + File.separator + fileName);
        			
                    //6.使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
        			byte[] bytes = new byte[1024];
        			int len = 0;
        			while ((len = is.read(bytes)) != -1){
    
    
        			    //7.使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件,写到服务器的硬盘中保存
        			    fos.write(bytes, 0, len);
        			}
                    
        			//8.使用客户端Socket对象中的方法getOutputStream,获取网络字节输出流OutputStream对象
        			//9.使用网络字节输出流OutputStream对象中的方法write,给客户端回写"上传成功!"
        			socket.getOutputStream().write("上传成功".getBytes());
			
        			//10.释放资源(fos, Socket, ServerScoket)
        			fos.close();
        			socket.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
        
        //让服务器一直启动,不在关闭了
        //server.close();
    }
}

File upload blocking problem

insert image description here

/*
    解决:上传完图片之后,给服务器写一个结束标记,告之服务器文件已经上传完毕,无需在等待了
    Socket对象中的方法
        void shutdownOutput() 禁用此套接字的输出流。
        对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列
 */
socket.shutdownOutput();

Channel

Buffer class (buffer)

java.nio.Buffer (abstract class): A container for data of a particular primitive type (basic type). When the Channel communicates, the bottom layer all uses Buffer.

Its several subclasses:

  • ByteBuffer : A byte[] array can be encapsulated inside. 【Key to master】
  • ShortBuffer: It can encapsulate a short[] array inside.
  • CharBuffer: It can encapsulate a char[] array inside
  • IntBuffer: An int[] array can be encapsulated inside.
  • LongBuffer: It can encapsulate a long[] array inside.
  • FloatBuffer: A float[] array can be encapsulated inside.
  • DoubleBuffer: A double[] array can be encapsulated inside.
  • There is no Buffer corresponding to boolean type

Attributes

  • Capacity (capacity): The maximum number of elements that the Buffer can contain. After a Buffer is defined, its capacity is immutable.

    int capacity()		//返回此缓冲区的容量
    
  • limit (limit): Indicates that if the setting is "limited to a certain position, then this position and the following positions will not be available".

    int limit()					// 获取此缓冲区的限制
    Buffer limit(int newLimit)	// 设置此缓冲区的限制
    
  • position (position): The current writable index. Position cannot be less than 0 and cannot be greater than 'limit'.

    int position()				// 获取当前可写入位置索引
    Buffer position(int p)		// 更改当前可写入位置索引
    
  • mark (mark): When the reset() method of the buffer is called, the position of the buffer will be reset to the index set by the mark.

    ​ Cannot be less than 0, cannot be greater than position.

    Buffer mark() 		// 在此缓冲区的当前位置(索引)设置标记
    Buffer reset() 		// 将此缓冲区的位置重置为以前标记的位置
    					/* 当我们调用缓冲区的reset方法时,会将缓冲区的position索引位置重置为mark标记的位置 */
    

Create ByteBuffer

java.nio.ByteBuffer: byte buffer, which encapsulates an array of byte type

The way to create objects: using static methods

static ByteBuffer allocate(int capacity)		// 使用一个“容量”来创建一个“间接字节缓存区”——程序的“堆”空间中创建。
static ByteBuffer allocateDirect(int capacity)	// 使用一个“容量”来创建一个“直接字节缓存区”——系统内存。
    											// 可以直接和系统交互,效率高
static ByteBuffer wrap(byte[] byteArray)		// 使用一个“byte[]数组”创建一个“间接字节缓存区”。

ByteBuffer member methods

/* 向ByteBuffer添加数据 */
ByteBuffer put(byte b)				// 向当前可用位置添加数据,一次添加一个字节
ByteBuffer put(byte[] byteArray)	// 向当前可用位置添加一个byte[]数组
ByteBuffer put(byte[] byteArray, int offset, int len)		// 添加一个byte[]数组的一部分
	/* 参数:int offset:数组的开始索引,从哪个索引开始添加
        	int len:添加个数
	*/
ByteBuffer put(int index, byte b) 	// 往指定索引处添加一个byte字节(替换)

byte[] array()		// 获取此缓冲区的 byte 数组

boolean isReadOnly()	// 获取当前缓冲区是否只读
boolean isDirect()		// 获取当前缓冲区是否为直接缓冲区
int remaining()			// 获取position与limit之间的元素数量
Buffer clear()			// 还原缓冲区的状态。
	/* 	将position设置为0
		将限制limit设置为容量capacity
		丢弃标记mark
	*/
Buffer flip()		// 缩小limit的范围。获取读取的有效数据0到position之间的数据
	/* 	将limit设置为当前position位置; [0, 1, 2, 0, 0, 0, 0, 0, 0, 0]  position=3 limit=10
		将当前position位置设置为0;   position=0 limit=3  new String(bytes, 0, len)
		丢弃标记
	*/

Example:

import java.nio.ByteBuffer;
import java.util.Arrays;

public class Demo02put {
    
    
    public static void main(String[] args) {
    
    
        //创建一个长度为10的ByteBuffer ==> 包含了一个长度为10的数组,默认值:{0,0,0,..0}
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println(buffer);		//java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]

        //- byte[] array()获取此缓冲区的 byte 数组
        byte[] arr = buffer.array();
        System.out.println(Arrays.toString(arr));//[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

        //- public ByteBuffer put(byte b):向当前可用位置添加数据,一次添加一个字节
        buffer.put((byte)1);	//1默认是int类型,put方法的参数需要byte类型,需要强转
        byte b1 = 2;
        buffer.put(b1);
        System.out.println(Arrays.toString(arr));//[1, 2, 0, 0, 0, 0, 0, 0, 0, 0]

        //- public ByteBuffer put(byte[] byteArray):向当前可用位置添加一个byte[]数组
        byte[] bytes = {
    
    10,20,30,40,50};
        buffer.put(bytes);
        System.out.println(Arrays.toString(arr));//[1, 2, 10, 20, 30, 40, 50, 0, 0, 0]

        /*
            - public ByteBuffer put(byte[] byteArray,int offset,int len):添加一个byte[]数组的一部分
            int offset:数组的开始索引,从哪个索引开始添加
            int len:添加个数
         */
        buffer.put(bytes,3,2);//40,50
        System.out.println(Arrays.toString(arr));//[1, 2, 10, 20, 30, 40, 50, 40, 50, 0]

        //ByteBuffer put(int index, byte b) 往指定索引处添加一个byte字节(替换)
        buffer.put(1,(byte)88);
        System.out.println(Arrays.toString(arr));//[1, 88, 10, 20, 30, 40, 50, 40, 50, 0]
        
        
        //创建一个长度为10的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put((byte)0);
        buffer.put((byte)1);
        buffer.put((byte)2);
        System.out.println(Arrays.toString(buffer.array()));//[0, 1, 2, 0(positon), 0, 0, 0, 0, 0, 0(limit 10)]

        //public int remaining():获取position与limit之间的元素数量。
        System.out.println(buffer.remaining());//7[3-9]

        //public boolean isReadOnly():获取当前缓冲区是否只读。
        System.out.println(buffer.isReadOnly());//false:既能读,有能写 true:只能读,不能写(只读)

        //public boolean isDirect():获取当前缓冲区是否为直接缓冲区。
        System.out.println(buffer.isDirect());//false:间接字节缓冲区(堆) true:直接字节缓冲区(系统)
        System.out.println(ByteBuffer.allocateDirect(10).isDirect());//true

        buffer.limit(5);//设置限制为5
        System.out.println("位置:"+buffer.position()+" 限制:"+buffer.limit());//位置:3 限制:5

        /*
            public Buffer clear():还原缓冲区的状态。
            - 将position设置为:0
            - 将限制limit设置为容量capacity;
            - 丢弃标记mark。
         */
        //buffer.clear();
        //System.out.println("位置:"+buffer.position()+" 限制:"+buffer.limit());//位置:0 限制:10
        //System.out.println(Arrays.toString(buffer.array()));//[0, 1, 2, 0, 0, 0, 0, 0, 0, 0]

        /*
            public Buffer flip():缩小limit的范围。 获取读取的有效数据:0到position之间的数据
            - 将limit设置为当前position位置; [0, 1, 2, 0, 0, 0, 0, 0, 0, 0]  position=3 limit=10
            - 将当前position位置设置为0;   position=0 limit=3  new String(bytes,0,len)
            - 丢弃标记。
         */
        buffer.flip();
        System.out.println("位置:"+buffer.position()+" 限制:"+buffer.limit());//位置:0 限制:3
    }
}

Channel overview

1).java.nio.channels. Channel (interface): connection for I/O operations.

  • Means: channel.
  • Can be "File Channel - FileChannel", "Network Channel - SocketChannel and ServerSockecChannel".
  • It is similar to IO streams, but more powerful than IO streams. read(byte[]) write(byte[])
  • IO flow is "one-way", and Channel is "two-way".

2). All Channels use Buffer to read and write. read(ByteBuffer) write(ByteBuffer)


BIO / FileChannel (file channel)

java.nio.channels.FileChannel: A channel for reading, writing, mapping, and manipulating files.

​ Similar to IO stream, used to read files, write files, copy files

FileChannel is an abstract class and cannot directly create objects. The way to obtain objects is:

/* 使用字节输入流(FileInputStream)中的方法获取读取文件的FileChannel */
FileChannel getChannel() 	// 返回与此文件输入流有关的唯一 FileChannel 对象
/* 使用字节输出流(FileOutputStream)中的方法获取写入文件的FileChannel */
FileChannel getChannel() 	// 返回与此文件输出流有关的唯一 FileChannel 对象

Member methods of FileChannel:

int read(ByteBuffer dst)  	// 读取多个字节存储到ByteBuffer中,相当于FileInputStream中的read(byte[])
int write(ByteBuffer src)  	// 将ByteBuffer中的数据写入到文件中,相当于FileOutputStream中的write(byte[])

Example:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Demo01FileChannel {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //1.创建FileInputStream对象,构造方法中绑定要读取的数据源
        FileInputStream fis = new FileInputStream("c:\\1.jpg");
        //2.创建FileOutputStream对象,构造方法中绑定要写入的目的地
        FileOutputStream fos = new FileOutputStream("d:\\1.jpg");
        //3.使用FileInputStream对象中的方法getChannel,获取读取文件的FileChannel对象
        FileChannel fisChannel = fis.getChannel();
        //4.使用FileOutputStream对象中的方法getChannel,获取写入文件的FIleChannel对象
        FileChannel fosChannel = fos.getChannel();
        //一读一写复制文件
        //5.使用读取文件的FileChannel对象中的方法read,读取文件 int read(ByteBuffer dst)
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int len = 0;
        while ((len = fisChannel.read(buffer)) != -1){
    
    
            //6.使用写入文件的FIleChannel对象中的方法write,把读取到的数据写入到文件中 int write(ByteBuffer src)
            //使用flip方法缩小limit的范围:最后一次读取的不一定是1024个字节
            System.out.println("flip前:position的位置:" + buffer.position() + ",limit:" + buffer.limit());
            buffer.flip();
            System.out.println("flip后:position的位置:" + buffer.position() + ",limit:" + buffer.limit());
            //write方法是从position(0)开始往目的地写数据,写到limit(有效字节),写完之后positon会变的
            System.out.println("write方法写数据:从" + buffer.position() + "写到:" + buffer.limit());
            fosChannel.write(buffer);//position(0)-limit(有效字节)之间的数据
            //初始化ByteBuffer的状态
            System.out.println("clear前:position的位置:" + buffer.position() + ",limit:" + buffer.limit());
            buffer.clear();//将position设置为0,将limit设置为容量(1024)
            System.out.println("clear后:position的位置:" + buffer.position()+",limit:" + buffer.limit());
            System.out.println("-----------------------------------------------");
        }
        //7.释放资源
        fosChannel.close();
        fisChannel.close();
        fos.close();
        fis.close();
    }
}

MappedByteBuffer: efficient reading and writing

java.io.RandomAccessFile class : IO stream class that can set read and write modes

Construction method:

public RandomAccessFile(String name, String mode)
/* 参数:
		String name;要读取的数据源,或者写入的目的地
		String mode:设置流的读写模式
			"r":只读,必须是小写
			"rw":读写,必须是小写
*/

Use the method getChannel in the RandomAccessFile class to get the FileChannel

FileChannel getChannel() 	// 返回与此文件关联的唯一 FileChannel 对象

Use the method map in the FileChannel class to get MappedByteBuffer

MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)  // 将此通道的文件区域直接映射到内存中
/* 参数:
		FileChannel.MapMode mode:设置读写的模式
			READ_ONLY:只读映射模式
			READ_WRITE:读取/写入映射模式
	   long position:文件中的位置,映射区域从此位置开始,一般都是从0开始
	   long size:要映射的区域大小,就是要复制文件的大小,单位字节
*/

java.nio.MappedByteBuffer : This creates a "direct buffer" that maps a file's disk data into memory .

​ In the system memory, directly interact with the system data, fast and efficient

Notice:

  • It can map at most: about Integer.MAX_VALUE bytes (2G). The directly copied file cannot exceed 2G. If it exceeds, it needs to be copied in blocks.

  • Disk and memory real-time mapping: When the files in the direct buffer in the memory change, the files in the mapped hard disk will also change in real time. High efficiency (memory and memory for reading and writing)

Member methods in MappedByteBuffer:

byte get(int index)  				// 获取缓冲区中指定索引处的字节
ByteBuffer put(int index, byte b)	// 把字节写入到指定的索引处

Example: Use FileChannel combined with MappedByteBuffer to achieve efficient reading, writing and copying of files above 2g

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Demo03FileChannel {
    
    
    public static void main(String[] args) throws IOException {
    
    
        long s = System.currentTimeMillis();
        //1.创建读取文件的RandomAccessFile对象,构造方法中封装要读取的数据源和设置只读模式("r")
        RandomAccessFile inRAF = new RandomAccessFile("c:\\2g.rar", "r");
        //2.创建写入文件的RandomAccessFile对象,构造方法中封装要写入的目的地和设置读写模式("rw")
        RandomAccessFile outRAF = new RandomAccessFile("d:\\2g.rar", "rw");
        //3.使用读取文件的RandomAccessFile对象中的方法getChannel,获取读取文件的FileChannel对象
        FileChannel inRAFChannel = inRAF.getChannel();
        //4.使用写入文件的RandomAccessFile对象中的方法getChannel,获取读取写入的FileChannel对象
        FileChannel outRAFChannel = outRAF.getChannel();
        //5.使用读取文件的FileChannel对象中的方法size,获取读取文件的大小(单位字节)
        long size = inRAFChannel.size();
        System.out.println(size);//2355126731 字节

        //定义复制文件需要使用的变量
        long count = 1;  //复制文件的块数,默认值是1
        long startIndex = 0;  //每次复制每块文件的开始索引
        long everySize = 512*1024*1024;  //分块,每块的大小 512M
        long copySize = size;  //每次复制文件的大小,默认等于文件的总大小

        //判断要复制的文件大小是否大于每块文件的大小
        if(size > everySize){
    
    
            //复制的文件大于512M,进行分块
            //计算复制文件可以分成几块  2242.56M/512M
            count = size%everySize==0 ? size/everySize : size/everySize+1;
            System.out.println("文件的大小:" + size + "字节,可以分成:" + count + "块");

            //第一次复制文件的大小等于每块的大小
            copySize = everySize;
        }

        //定义一个for循环,分几块就循环复制几次
        for (int i = 0; i < count; i++) {
    
    
            //6.使用读取文件的FileChannel对象中的方法map,创建读取文件的直接缓冲区MappedByteBuffer对象
            MappedByteBuffer inMap = inRAFChannel.map(FileChannel.MapMode.READ_ONLY, startIndex, copySize);
            //7.使用写入文件的FileChannel对象中的方法map,创建写入文件的直接缓冲区MappedByteBuffer对象
            MappedByteBuffer outMap = outRAFChannel.map(
                FileChannel.MapMode.READ_WRITE, startIndex, copySize);
            System.out.println("每块文件的开始复制的索引:" + startIndex);
            System.out.println("每块文件的大小:" + copySize + "字节");
            System.out.println("--------------------------------------------");
            //8.创建for循环,循环size次
            for (int j = 0; j < copySize; j++) {
    
    
                //9.使用取文件的直接缓冲区MappedByteBuffer对象中的方法get,读取数据源指定索引处的文件
                byte b = inMap.get(j);
                //10.使用写入文件的直接缓冲区MappedByteBuffer对象中的方法put,把读取到的字节写入到目的地指定的索引处
                outMap.put(j, b);
            }

            //复制完每块文件,重新计算startIndex和copySize的大小
            startIndex += copySize;
            copySize = size-startIndex>everySize ? everySize : size-startIndex;
        }

        //11.释放资源
        outRAFChannel.close();
        inRAFChannel.close();
        outRAF.close();
        inRAF.close();
        long e = System.currentTimeMillis();
        System.out.println("复制文件共耗时:"+(e-s)+"毫秒!");//复制文件共耗时:4912毫秒!
    }
}

NIO / SocketChannel (network channel)

Service-Terminal

Related classes : java.nio.channels.ServerSocketChannel : An optional channel for a stream-oriented listening socket.

The way to get the object: use the static method open

static ServerSocketChannel open() 		// 打开服务器插槽通道

Member method:

ServerSocketChannel bind(SocketAddress local) 		// 给服务器绑定指定的端口号
SocketChannel accept() 		// 监听客户端的请求
SelectableChannel configureBlocking(boolean block) 		// 设置服务器的阻塞模式 true:阻塞(不写默认) false:非阻塞

client

Related classes : java.nio.channels.SocketChannel : An optional channel for a stream-oriented connection socket.

The method of obtaining the object: use the static method open

static SocketChannel open() 		// 打开套接字通道

Member method:

boolean connect(SocketAddress remote) 		// 根据服务器的ip地址和端口号连接服务器
/* 参数:
		SocketAddress remote:封装服务器的ip地址和端口号,用的时候直接new
            返回值:boolean
                连接服务器成功:true
                连接服务器失败:false
*/
SelectableChannel configureBlocking(boolean block) 	// 设置客户端的阻塞模式。true:阻塞(不写默认) false:非阻塞
/*
	1.客户端设置为阻塞模式:connect方法会多次尝试连接服务器
		connect连接成功服务器,返回true
		connect连接服务器失败,会抛出连接异常ConnectException: Connection refused: connect
	2.客户端设置为非阻塞模式:connect方法只会连接一次服务器
		connect方法无论连接是否成功还是失败都会false
		所以客户端设置为非阻塞模式没有意义,结束不了轮询
*/
    
int write(ByteBuffer src) 		// 给服务器发送数据
int read(ByteBuffer dst) 		// 读取服务器回写的数据

example

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/*
    实现同步非阻塞的服务器:轮询监听客户端的请求
 */
public class NIOTCPServer {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.使用open方法获取ServerSocketChannel对象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2.使用ServerSocketChannel对象中的方法bind给服务器绑定指定的端口号
        serverSocketChannel.bind(new InetSocketAddress(8888));

        //SelectableChannel configureBlocking(boolean block) 设置服务器的阻塞模式 true:阻塞(不写默认) false:非阻塞
        serverSocketChannel.configureBlocking(false);

        //轮询监听客户端的请求 ==> 死循环一直执行,监听客户端
        while (true){
    
    
            //3.使用ServerSocketChannel对象中的方法accept监听客户端的请求
            System.out.println("服务器等待客户端的连接...");
            SocketChannel socketChannel = serverSocketChannel.accept();//accpet:非阻塞 不会等待客户端请求

            //对客户端SocketChannel对象进行一个非空判断,没有客户端连接服务器,accpet方法返回null
            if(socketChannel != null){
    
    
                System.out.println("有客户端连接服务器,服务器读取客户端发送的数据,给客户端回写数据...");

                //int read(ByteBuffer dst) 读取客户端发送的数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int len = socketChannel.read(buffer);
                String msg = new String(buffer.array(), 0, len);
                System.out.println("服务器读取客户端发送的数据:" + msg);

                //int write(ByteBuffer src) 服务器给客户端发送数据
                socketChannel.write(ByteBuffer.wrap("收到,谢谢".getBytes()));

                System.out.println("服务器读写数据完成,结束轮询...");
                break;//结束轮询
            }else{
    
    
                System.out.println("没有客户端连接服务器,休息2秒钟,干点其他事情,在继续下一次轮询监听客户端连接...");
                Thread.sleep(2000);
            }
        }

        //释放资源
        serverSocketChannel.close();
    }
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/*
    客户端:轮询连接服务器。客户端轮询连接服务器成功,给服务器发送数据,读取服务器回写的数据
 */
public class NIOTCPClient {
    
    
    public static void main(String[] args) {
    
    
        //客户端轮询连接服务器,连接成功,结束轮询
        while (true){
    
    
            try {
    
    
                //1.使用open方法获取客户端SocketChannel对象
                SocketChannel socketChannel = SocketChannel.open();
                System.out.println("客户端开始连接服务器...");

                //2.使用SocketChannel对象中的方法connect根据服务器的ip地址和端口号连接服务器
                boolean b = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
                System.out.println(b);
                System.out.println("客户端连接服务器成功,给服务器发送数据,读取服务器回写的数据...");
                //int write(ByteBuffer src) 给服务器发送数据
                ByteBuffer buffer = ByteBuffer.wrap("你好服务器".getBytes());
                System.out.println("容量:"+buffer.capacity());
                System.out.println("索引:"+buffer.position());
                System.out.println("限定:"+buffer.limit());
                socketChannel.write(buffer);

                //int read(ByteBuffer dst) 读取服务器回写的数据
                ByteBuffer buffer2 = ByteBuffer.allocate(1024);
                int len = socketChannel.read(buffer2);
                //len:读取的有效字节个数
                //System.out.println("客户端读取服务器发送的数据:" + new String(buffer2.array(), 0, len));

                buffer2.flip();//缩小limit的范围: position=0 limit=position(读取的有效字节个数)
                System.out.println("客户端读取服务器发送的数据:"+new String(buffer2.array(),0,buffer2.limit()));

                System.out.println("客户端读写数据完毕,结束轮询...");
                break;
            } catch (IOException e) {
    
    
                System.out.println("客户端connect方法连接服务器失败,休息2秒钟,干点其他事情,在继续下一次连接服务器...");
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e1) {
    
    
                    e1.printStackTrace();
                }
            }
        }
    }
}

Multiplexer/Selector: Selector

The concept of multiplexing

Selector Selector is one of the important technologies in NIO. It is used in conjunction with SelectableChannel to implement non-blocking multiplexing. Using it can save CPU resources and improve the operating efficiency of the program.

"Multi-channel" refers to the situation where the server listens to multiple "ports" at the same time. Each port listens for connections from multiple clients.

  • Server-side non-multiplexing effects

    insert image description here

    If you don't use "multiplexing", the server needs to open many threads to process requests for each port. If it is in a high concurrency environment, the system performance will drop.

  • Multiplexing effects on the server side

    insert image description here

    Using multiplexing, only one thread can process multiple channels, reduce memory usage, reduce CPU switching time, and have very important advantages in high-concurrency and high-band business environments


Selector Selector_ server side to achieve multi-way registration

Related classes: java.nio.channels.Selector : A multiplexer for SelectableChannel objects.

The way to get the Selector object:

static Selector open() 		// 打开选择器

Register Channel (server channel) to Selector: use the method in ServerSocketChannel

SelectionKey register(Selector sel, int ops) 		// 使用给定的选择器注册此频道,返回一个选择键。
/* 参数:
   	Selector sel:传递要注册的选择器对象
   	int ops:传递对应的事件
       	使用SelectionKey中的常量:SelectionKey.OP_ACCEPT(固定写法,把服务器通道注册到选择器上)
           OP_ACCEPT:监听客户端件事
*/

Selector Selector_ common methods

// 返回一个Set<SelectionKey>集合,表示:已注册通道的集合。每个已注册通道封装为一个SelectionKey对象
Set<SelectionKey> keys()

// 返回一个Set<SelectionKey>集合,表示:当前已连接的通道的集合。每个已连接通道同一封装为一个SelectionKey对象
Set<SelectionKey> selectedKeys()
    
int select()	// 会阻塞,直到至少有1个客户端连接。返回一个int值,表示有几个客户端连接了服务器。

Example: Multi-channel message reception

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/*
    服务器
 */
public class TCPServer {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.创建3个ServerSocketChannel服务器对象
        ServerSocketChannel channel01 = ServerSocketChannel.open();
        ServerSocketChannel channel02 = ServerSocketChannel.open();
        ServerSocketChannel channel03 = ServerSocketChannel.open();
        //2.分别给3个ServerSocketChannel服务器对象绑定不同的端口号
        channel01.bind(new InetSocketAddress(7777));
        channel02.bind(new InetSocketAddress(8888));
        channel03.bind(new InetSocketAddress(9999));
        //3.设置3个ServerSocketChannel服务器对象为非阻塞模式(只要使用Selector选择器,服务器必须是非阻塞的)
        channel01.configureBlocking(false);
        channel02.configureBlocking(false);
        channel03.configureBlocking(false);
        //4.获取Selector对象
        Selector selector = Selector.open();
        //5.使用服务器ServerSocketChannel对象中的方法register,把3个服务器通道注册到选择器Selector上
        channel01.register(selector, SelectionKey.OP_ACCEPT);
        channel02.register(selector, SelectionKey.OP_ACCEPT);
        channel03.register(selector, SelectionKey.OP_ACCEPT);

        //Selector的keys()方法 此方法返回一个Set<SelectionKey>集合,表示:已注册通道的集合。每个已注册通道封装为一个SelectionKey对象。
        Set<SelectionKey> keys = selector.keys();
        System.out.println("已注册服务器通道的数量:" + keys.size());	//3

        //服务器轮询监听客户端的请求
        while (true){
    
    
            //Selector的select()方法:获取客户端连接的数量,没有客户端连接服务器,此方法会一直阻塞
            int select = selector.select();
            System.out.println("连接服务器的客户端数量:" + select);

            //Selector的selectedKeys()方法:获取当前已经连接的通道集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            System.out.println("已经到服务器的通道数量:" + selectionKeys.size());

            //处理Selector监听到客户端的请求事件:遍历Set集合,获取每一个SelectionKey对象
            Iterator<SelectionKey> it = selectionKeys.iterator();
            while (it.hasNext()){
    
    
                SelectionKey selectionKey = it.next();
                //获取SelectionKey里边封装的服务器ServerSocketChannel对象
                ServerSocketChannel channel = (ServerSocketChannel)selectionKey.channel();
                System.out.println("获取当前通道ServerSocketChannel监听的端口号:" + channel.getLocalAddress());
                //处理监听的accept事件==>获取请求服务器的客户单SocketChannel对象
                SocketChannel socketChannel = channel.accept();
                //读取客户端SocketChannel发送的数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int len = socketChannel.read(buffer);
                System.out.println("服务器读取到客户端发送的数据:" + new String(buffer.array(), 0, len));

                //处理完SelectionKey监听到的事件,要在Set集合中移除已经处理完的SelectionKey对象
                it.remove();//使用迭代器对象移除集合中的元素,不会抛出并发修改异常(移除的就是it.next()方法取出的对象)
            }

            //获取完一个客户端的连接,睡眠2秒,在进行下一次轮询
            Thread.sleep(2000);
        }
    }
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/*
    开启三个线程,每个线程分别创建一个客户端对象,连接服务器的三个端口
 */
public class TCPClient {
    
    
    public static void main(String[] args) {
    
    
        new Thread(()->{
    
    
            //创建客户端对象,轮询连接服务器
            while (true){
    
    
                try(SocketChannel socketChannel = SocketChannel.open();) {
    
    
                    System.out.println("客户端开始连接7777端口...");
                    socketChannel.connect(new InetSocketAddress("127.0.0.1",7777));

                    System.out.println("客户端连接7777端口成功,给服务器发送数据");
                    socketChannel.write(ByteBuffer.wrap("你好服务器,我是连接7777端口号的客户端对象!".getBytes()));

                    System.out.println("客户端7777发送数据完毕,结束轮询...");
                    break;
                } catch (IOException e) {
    
    
                    System.out.println("客户端连接7777端口异常");
                }
            }
        }).start();

        new Thread(()->{
    
    
            //创建客户端对象,轮询连接服务器
            while (true){
    
    
                try(SocketChannel socketChannel = SocketChannel.open();) {
    
    
                    System.out.println("客户端开始连接8888端口...");
                    socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));

                    System.out.println("客户端连接8888端口成功,给服务器发送数据");
                    socketChannel.write(ByteBuffer.wrap("你好服务器,我是连接8888端口号的客户端对象!".getBytes()));

                    System.out.println("客户端8888发送数据完毕,结束轮询...");
                    break;
                } catch (IOException e) {
    
    
                    System.out.println("客户端连接8888端口异常");
                }
            }
        }).start();

        new Thread(()->{
    
    
            //创建客户端对象,轮询连接服务器
            while (true){
    
    
                try(SocketChannel socketChannel = SocketChannel.open();) {
    
    
                    System.out.println("客户端开始连接9999端口...");
                    socketChannel.connect(new InetSocketAddress("127.0.0.1",9999));

                    System.out.println("客户端连接9999端口成功,给服务器发送数据");
                    socketChannel.write(ByteBuffer.wrap("你好服务器,我是连接9999端口号的客户端对象!".getBytes()));

                    System.out.println("客户端9999发送数据完毕,结束轮询...");
                    break;
                } catch (IOException e) {
    
    
                    System.out.println("客户端连接9999端口异常");
                }
            }
        }).start();
    }
}

AIO / Asynchronous Channel: AsynchronousServerSocketChannel

overview

Server side : java.nio.channels.AsynchronousServerSocketChannel : An asynchronous channel for a stream-oriented listening socket .

The method to get the object:

static AsynchronousServerSocketChannel open() 	// 打开异步服务器套接字通道

Member method:

AsynchronousServerSocketChannel bind(SocketAddress local) 	// 给服务器绑定指定的端口号
void accept(A attachment, CompletionHandler<?> handler) 	// 监听客户端的请求,默认就是非阻塞的
/* 参数:
        A attachment:附件,可以传递null
        CompletionHandler<?> handler:事件处理的接口,用于处理accept方法监听到的事件
*/
    
Future<Integer> write(ByteBuffer src) 			// 给客户端发送数据。阻塞方法,会一直等待客户端发送数据
// 读取客户端发送的数据。非阻塞的读取方法
Future<Integer> read(ByteBuffer dst)
void read(ByteBuffer dst, long timeout, TimeUnit unit, A attachment, CompletionHandler<?> handler)
/* 参数:
		ByteBuffer dst: 用来存储读取到的数据
		long timeout: 完成I / O操作的最长时间
		TimeUnit unit: timeout参数的时间单位(TimeUnit.SECONDS:秒)
		A attachment: 要附加到I / O操作的对象; 可以是null
		CompletionHandler<?> handler: 消费结果的处理程序,是一个回调函数
*/

java.nio.channels.CompletionHandler <V, A> Interface: A handler used to dismiss the results of asynchronous I/O operations .

CompletionHandler is also called a callback function. If the accept or read method is executed successfully, the server will automatically execute this callback function to read the data sent by the client.

Methods in the interface:

void completed(V result, A attachment) 		// 主方法执行成功,执行的方法
void failed(Throwable exc, A attachment) 	// 主方法执行失败,执行的方法

Clients : java.nio.channels.AsynchronousSocketChannel : An asynchronous channel for stream-oriented connection sockets.

The method to get the object:

static AsynchronousSocketChannel open() 		// 打开异步套接字通道

Member method:

Future<Void> connect(SocketAddress remote) 		// 连接服务器的方法,参数传递服务器的ip地址和端口号
/* 注意:
		connect是一个非阻塞的方法,不会等待方法运行完毕,连接服务器成功在执行下边的代码
		客户端连接服务器需要时间的,如果没有连接成功,就给服务器使用write方法发送数据,会抛出异常
*/

Future<Integer> write(ByteBuffer src) 			// 给服务器发送数据
Future<Integer> read(ByteBuffer dst) 			// 读取服务器发送的数据

java.util.concurrent.Future interface _

Methods in the interface:

boolean isDone() 			// 如果此任务完成,则返回 true
/*	返回true:连接服务器成功
    返回false:还没有连接上服务器(客户端连接服务器是需要时间的)
*/

Example: AIO asynchronous connection, asynchronous blocking read and write

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/*
	服务器端
*/
public class AIOTCPServer {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.获取异步非阻塞的服务器AsynchronousServerSocketChannel对象
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
        //2.使用bind方法给AsynchronousServerSocketChannel对象绑定指定的端口号
        serverSocketChannel.bind(new InetSocketAddress(8888));
        //3.使用AsynchronousServerSocketChannel对象中的方法accept监听客户端的请求
        System.out.println("accept方法开始执行了...");
        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
    
    
            @Override
            public void completed(AsynchronousSocketChannel result, Object attachment) {
    
    
                System.out.println("客户端连接服务器成功...");
                //读取客户端发送的数据  Future<Integer> read(ByteBuffer dst)
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //read方法是一个阻塞的方法,会一直等待客户端发送数据
                Future<Integer> future = result.read(buffer);
                Integer len = 0;
                try {
    
    
                    len = future.get();//获取客户端发送数据长度
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (ExecutionException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("服务器读取客户端发送的数据:"+new String(buffer.array(),0,len));
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
    
    
                System.out.println("客户端连接服务器失败...");
            }
        });
        System.out.println("accept方法执行结束了...");
        //accept方法是一个非阻塞的方法,我们执行完accept方法,可以去做其他的事情
        //死循环的目的不让程序停止(工作中:写一些具体的需求);当有客户端请求服务器,会自动执行回调函数中的方法
        while (true){
    
    
            System.out.println("正在忙其他的事情!");
            Thread.sleep(2000);
        }
    }
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.Future;

/*
	客户端
 */
public class AIOTCPClient {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.创建异步非阻塞的客户端AsynchronousSocketChannel对象
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        //2.使用AsynchronousSocketChannel对象中的方法connect连接服务器
        Future<Void> future = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
        System.out.println(future.isDone());	//false:还未连接上服务器
        System.out.println(111);
        //休眠5秒钟,等待客户端连接服务器成功,再给服务器发送数据
        Thread.sleep(5000);
        System.out.println(future.isDone());	//true:已经连接服务器成功
        if(future.isDone()){
    
    
            socketChannel.write(ByteBuffer.wrap("你好服务器".getBytes()));
        }
        //释放资源
        socketChannel.close();
    }
}

Example: AIO asynchronous connection, asynchronous non-blocking read and write

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/*
	服务器端
*/
public class AIOTCPServer {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.获取异步非阻塞的服务器AsynchronousServerSocketChannel对象
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
        //2.使用bind方法给AsynchronousServerSocketChannel对象绑定指定的端口号
        serverSocketChannel.bind(new InetSocketAddress(8888));
        //3.使用AsynchronousServerSocketChannel对象中的方法accept监听客户端的请求
        System.out.println("accept方法开始执行了...");
        serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
    
    
            @Override
            public void completed(AsynchronousSocketChannel result, Object attachment) {
    
    
                System.out.println("客户端连接服务器成功...");
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //当客户端连接服务器成功,read方法只会等待客户端10秒钟,10秒钟发送了数据,执行completed方法;10秒钟之后还没有发送数据,执行failed
                result.read(buffer, 10, TimeUnit.SECONDS, null, new CompletionHandler<Integer, Object>() {
    
    
                    @Override
                    public void completed(Integer result, Object attachment) {
    
    
                        System.out.println("服务器读取客户端发送数据成功,执行completed方法");
                        //服务器读取客户端发送的数据
                        String msg = new String(buffer.array(), 0, result);//把读取到的有效数据,转换为字符串
                        System.out.println("服务器读取客户端发送的数据为:"+msg);
                    }

                    @Override
                    public void failed(Throwable exc, Object attachment) {
    
    
                        System.out.println("服务器读取客户端发送数据失败,执行failed方法");
                    }
                });

            }

            @Override
            public void failed(Throwable exc, Object attachment) {
    
    
                System.out.println("客户端连接服务器失败...");
            }
        });
        System.out.println("accept方法执行结束了...");
        //accept方法是一个非阻塞的方法,我们执行完accept方法,可以去做其他的事情
        //死循环的目的不让程序停止(工作中:写一些具体的需求);当有客户端请求服务器,会自动执行回调函数中的方法
        while (true){
    
    
            System.out.println("正在忙其他的事情!");
            Thread.sleep(2000);
        }
    }
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.Future;

/*
	客户端
 */
public class AIOTCPClient {
    
    
    public static void main(String[] args) throws IOException, InterruptedException {
    
    
        //1.创建异步非阻塞的客户端AsynchronousSocketChannel对象
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        //2.使用AsynchronousSocketChannel对象中的方法connect连接服务器
        Future<Void> future = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
        System.out.println(future.isDone());//false:还未连接上服务器
        System.out.println(111);
        //休眠5秒钟,等待客户端连接服务器成功,再给服务器发送数据
        Thread.sleep(15000);
        System.out.println(future.isDone());//true:已经连接服务器成功
        if(future.isDone()){
    
    
            socketChannel.write(ByteBuffer.wrap("你好服务器".getBytes()));
        }
        //释放资源
        socketChannel.close();
    }
}

Socket communication

overview

There are dozens of communication protocols in the Linux kernel protocol cluster. AF-INET is a common TCP/IP communication method. AF-UNIX is an IPC mechanism used for communication between local threads. From the perspective of users, the communication modes adopted are not much different, but in principle, they are quite different.


AF_INET domain socket communication process

The communication process of a typical TCP/IP four-layer model:

insert image description here

The sender and receiver rely on IP:Port to identify, that is, bind the local socket to the corresponding IP port. When sending data, specify the IP port of the other party. After going through the Internet, you can finally find the receiver according to this IP port; when receiving data, you can get the IP port of the sender from the data packet.

The sender sends the original data to the operating system kernel buffer through the system call send(). The kernel buffer is encoded in the TCP layer, IP layer, and link layer from top to bottom, and the corresponding header information is added respectively, and a data packet is sent to the network through the network card. Routed through the network to the receiver's network card. The network card notifies the receiver's operating system of the data packet through a system interrupt, and then decodes it in the opposite direction of the sender's encoding, that is, through the link layer, IP layer, and TCP layer to remove the header, check and verify, etc., and finally report the original data to the receiver process.


AF_UNIX domain socket communication process

Typical local IPC, like pipes, relies on pathnames to identify sender and receiver. That is, when sending data, specify the path name bound to the receiver, and the operating system can directly find the corresponding receiver based on the path name, copy the original data directly to the receiver's kernel buffer, and report it to the receiver process for processing. The same receiver can obtain the path name of the sender from the received data packet, and send data to it through this path name.

insert image description here


Similarities and differences and application scenarios

Same point

  • The interfaces socket(), bind(), connect(), accept(), send(), recv() provided by the operating system, and select(), poll(), epoll() for multiplexing event detection are all exactly the same. In the process of sending and receiving data, the upper layer application cannot perceive the difference of the lower layer.

difference

  • There is a slight difference between the address field passed by the socket and the address structure of bind():

    • socket() passes different domains AF_INET and AF_UNIX respectively
    • The address structure of bind() is sockaddr_in (specify IP port) and sockaddr_un (specify path name)
  • AF_INET needs to be encoded and decoded by multiple protocol layers, which consumes the system cpu, and data transmission needs to pass through the network card, which is limited by the bandwidth of the network card. After the AF_UNIX data arrives in the kernel buffer, the kernel finds the kernel buffer corresponding to the receiver socket according to the specified path name, and directly copies the data to it without encoding and decoding at the protocol layer, saving system cpu, and does not pass through the network card, so it is not limited by the bandwidth of the network card.

  • The transfer rate of AF_UNIX is much higher than that of AF_INET

  • AF_INET can not only be used for cross-process communication of this machine, but also can be used for communication between different machines. It was born for network interconnection and data transfer between different machines. And AF_UNIX can only be used for communication between processes within the machine.


Application Scenario

  • AF_UNIX Because of its less consumption of system cpu, not limited by network card bandwidth, and efficient transmission rate, AF_UNIX domain is preferred for local communication
  • AF_INET is mostly used for communication between machines

AF UNIX Server Socket Communication

Java AFUNIXServerSocket class

Reference: https://vimsky.com/examples/detail/java-class-org.newsclub.net.unix.AFUNIXServerSocket.html

rely:

        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-core</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.kohlschutter.junixsocket</groupId>
            <artifactId>junixsocket-common</artifactId>
            <version>2.3.2</version>
        </dependency>

Example 1: run

import org.newsclub.net.unix.AFUNIXServerSocket; 

@AllArgsConstructor
public class SocketJob implements Runnable{
    
    
    private String path;
    
    public void run() throws IOException {
    
    
        File socketFile = new File(path);
        socketFile.deleteOnExit();

        final ExecutorService executorService = Executors.newCachedThreadPool();

        try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) {
    
    
            // 绑定路径
            server.bind(new AFUNIXSocketAddress(socketFile));
            System.out.println("server: " + server);

            while (!Thread.interrupted()) {
    
    
                System.out.println("Waiting for connection...");
                executorService.execute(new ClientConnection(this, server.accept()));
            }
        } finally {
    
    
            executorService.shutdown();
        }
    }
}

Usage example 2: main

import org.newsclub.net.unix.AFUNIXServerSocket; 

public static void main(String[] args) throws IOException {
    
    
    final File socketFile =
            new File(new File(System.getProperty("java.io.tmpdir")), "junixsocket-test.sock");

    try (AFUNIXServerSocket server = AFUNIXServerSocket.newInstance()) {
    
    
        server.bind(new AFUNIXSocketAddress(socketFile));
        System.out.println("server: " + server);

        while (!Thread.interrupted()) {
    
    
            System.out.println("Waiting for connection...");
            try (Socket sock = server.accept()) {
    
    
                System.out.println("Connected: " + sock);

                try (InputStream is = sock.getInputStream(); 
                     OutputStream os = sock.getOutputStream()) {
    
    
                    byte[] buf = new byte[128];
                    int read = is.read(buf);
                    System.out.println("Client's response: " + new String(buf, 0, read));

                    System.out.println("Saying hello to client " + os);
                    os.write("Hello, dear Client".getBytes());
                    os.flush();
                }
            }
        }
    }
}

socket communication message reception

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

/**
 * AF_UNIX 域 socket 通讯消息接收
 */
@AllArgsConstructor
@Slf4j
public class SocketMsgReceiveBO implements Runnable {
    
    

    /**
     * AF_UNIX 域 docker socket 客户端
     */
    private Socket socketClient;

    @Override
    public void run() {
    
    
        log.info("start socket client receive msg");

        if (socketClient == null || socketClient.isClosed()){
    
    
            log.error("socket client is unavailable");
            return;
        }

        StringBuffer acceptMsgBuffer = new StringBuffer();
        try (InputStream is = socketClient.getInputStream()) {
    
    
            byte[] buf = new byte[2048];
            int readLenth;
            // 退出当前循环依赖于socketClient端主动关闭链接,否则当前线程一致等待通讯
            while ((readLenth = is.read(buf)) != -1){
    
    
                acceptMsgBuffer.append(new String(buf, 0, readLenth, StandardCharsets.UTF_8));
                log.info("server accept msg:{}", acceptMsgBuffer);

                ArrayList<String> validJsonMsgList = new ArrayList<>();
                // 获取socket回调消息中的符合json格式的字符串
                String validJsonMsg = getValidJsonStrFromMsg(acceptMsgBuffer.toString());
                while (StringUtils.isNotBlank(validJsonMsg)){
    
    
                    // 添加到待解析消息集合中
                    validJsonMsgList.add(validJsonMsg);

                    /**
                     * 比较当前合法json格式的消息和原始消息的长度
                     * 若一致,则原始消息即为一个完整的符合json格式的消息,清空消息缓存Buffer
                     * 若不一致,则从原始消息的符合json格式片段的最后一个字符之后开始截取,等待拼接到下一次的消息
                     */
                    String originMsg = acceptMsgBuffer.toString();
                    if (validJsonMsg.length() == originMsg.length()){
    
    
                        acceptMsgBuffer = new StringBuffer();
                        break;
                    } else {
    
    
                        acceptMsgBuffer = new StringBuffer(originMsg.substring(validJsonMsg.length()));
                    }

                    log.info("all wait parse valid json msgs:{} and nextBuffer:{}", JSON.toJSONString(validJsonMsgList), acceptMsgBuffer);

                    // 解析合法的消息
                    validJsonMsgList.forEach(msg -> parseSocketMsg(msg));
                }

            }
        } catch (Exception e){
    
    
            log.error("failed to receive socket client msg", e);
        } finally {
    
    
            String msg = acceptMsgBuffer.toString();
            if (StringUtils.isNotBlank(msg) && isValidSocketMsg(msg)){
    
    
                log.info("finally parse last msg:{}", msg);
                parseSocketMsg(msg);
            }
            if (!socketClient.isClosed()){
    
    
                // 若socket客户端未自动关闭,则主动关闭
                try {
    
    
                    socketClient.close();
                    log.info("success to close socket client");
                } catch (IOException e){
    
    
                    log.error("failed to close socket client");
                }
            }
        }
    }

    /**
     * 获取socket回调消息中的合法json格式的字符串
     */
    private String getValidJsonStrFromMsg(String msg) {
    
    
        if (StringUtils.isBlank(msg)){
    
    
            return "";
        }
        /**
         * 为防止出现粘包现象,对该消息进行循环判断,一旦发现内部存在合法的json格式,即任务是有效的消息
         * 依赖每次对消息进行截取长度少1的方式,以保证当不能找到匹配的JSON也能退出循环
         */
        while (!isValidSocketMsg(msg) && StringUtils.isNoneBlank(msg)){
    
    
            // 从0开始截取到当前字符串的最后一个字符。substring(startIndex, endIndex)方法入参前闭后开区间
            msg = msg.substring(0, msg.length()-1);
            int lastLeftBraceIndex = msg.lastIndexOf("}");
            if (lastLeftBraceIndex < 0){
    
    
                // 当字符串中没有"}"字符时,退出循环
                break;
            }
            // 从0截取到字符串的最后一个"}"字符
            msg = msg.substring(0, lastLeftBraceIndex + 1);
        }
        if (isValidSocketMsg(msg)){
    
    
            return msg;
        }
        return "";
    }

    /**
     * 如果接收到的消息非空,且符合json格式,则认为是合法的消息,可以进行解析
     */
    private boolean isValidSocketMsg(String msg) {
    
    
        if(StringUtils.isBlank(msg)) return false;
        try {
    
    
            JSON.parse(msg);
            return true;
        } catch (Exception e){
    
    
            return false;
        }
    }

    /**
     * 解析符合json格式的消息
     */
    private void parseSocketMsg(String msgStr) {
    
    
        JSONObject msgJsonObj = JSON.parseObject(msgStr);
        // TODO 根据约定的通讯字段进行相关业务逻辑
    }
}

Guess you like

Origin blog.csdn.net/footless_bird/article/details/126289727