Java Socket编程

Posted 蚂蚁小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Socket编程相关的知识,希望对你有一定的参考价值。

一:Socket介绍

  Socket(中文:套接字)编程是网络常用的编程,我们通过在网络中创建Socket关键字来实现网络间的通信;要想解释清楚Socket,首先要知道TCP,要想知道TCP,那就得对TCP/IP的体系结构以及每一层的大概工作有所了解,那么我们就先来说说TCP/IP的分层。

1:ISO/OSI和TCP/IP模型

其实模型一共分为2种:
ISO/OSI模型:
  即开放式通信系统互联参考模型(Open System Interconnection Reference Model),是国际标准化组织(ISO)提出的一个试图
   使各种计算机在世界范围内互连为网络的标准框架,简称OSI。
TCP/IP协议模型:
  即包含了一系列构成互联网基础的网络协议(Transmission Control Protocol/Internet Protocol),是Internet的核心协议,
  通过20多年的发展已日渐成熟,并被广泛应用于局域网和广域网中,目前已成为事实上的国际标准。TCP/IP协议簇是一组不同层次上的
  多个协议的组合,通常被认为是一个四层协议系统,与OSI的七层模型相对应。
具体的TCP/IP四层模型,有兴趣可以看看:
应用层:
  应用层决定了向用户提供应用服务时通信的活动。应用层负责处理特定的应用程序细节。TCP/IP 协议族内预存了各类通用的应用服务。
  比如:FTP( Transfer Protocol,文件传输协议)和 DNS(Domain Name ,域名系统)服务就是其中两类。 
  HTTP 协议也处于该层。
传输层:
  传输层对上层应用层提供处于网络连接中的两台计算机之间的数据 传输。
  在传输层有两个性质不同的协议:
        TCP(Transmission Control Protocol传输控制协议)
        UDP(User Data Protocol用户数据报协议)
  这两个协议主要为两台主机上的应用程序提供端到端的通信。
  在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和UDP(用户数据报协议)。
  TCP:为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的
  分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。为了提供
  可靠的服务,TCP采用了超时重传、发送和接收端到端的确认分组等机制。
  UDP:则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。
  一个数据报是指从发送方传输到接收方的一个信息单元(例如,发送方指定的一定字节数的信息)。UDP协议任何必需的可靠性必须由应
  用层来提供。
网络层:
  网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计
  算机,并把数据包传送给对方。与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的作用就是在众多的选项内选择一
  条传输路线。也称作互联网层(在图中为网络层),处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议
  包括IP协议(网络协议),ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)。
  IP:是一种网络层协议,提供的是一种不可靠的服务,它只是尽可能快地把分组从源结点送到目的结点,但是并不提供任何可靠性保证。
    同时被TCP和UDP使用。TCP和UDP的每组数据都通过端系统和每个中间路由器中的IP层在互联网中进行传输。
  ICMP:是IP协议的附属协议。IP层用它来与其它主机或路由器交换错误报文和其它重要信息。
  IGMP:是Internet组管理协议。它用来把一个UDP数据报多播到多个主机。
链路层:
  用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及
  光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。也称作数据链路层或网络接口
  层(在第一个图中为网络接口层和硬件层),通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆
  (或其他任何传输媒介)的物理接口细节。ARP(地址解析协议)和RARP(逆地址解析协议)是某些网络接口(如以太网和令牌环网)
  使用的特殊协议,用来转换IP层和网络接口层使用的地址。

2:总结

链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;
网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;
传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;
应用层:定义数据格式,并按照对应的格式解读数据。
把每层模型的职责串联起来,用一句通俗易懂的话讲就是:
    当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了
    双方通信的应用程序;然后网络协议加上了双方的IP地址,确认了双方的网络位置;最后链路层协议加上了双方的MAC地址,确认了双
    方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会
    转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再
    通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。

二:JavaSE实现Socket网络编程

1:基本梳理

InetAddress类:
    表示Internet协议(IP)地址。可以通过此类获取IP地址对象
    其直接实现子类:Inet4Address(IPv4)、Inet6Address(IPv6)
    常用方法及属性:
        static InetAddress getLocalHost():返回本地主机的地址。
        String getHostName():获取此IP地址的主机名。  
    //获取指定IP地址
    InetAddress id = InetAddress.getByName("49.235.99.193");
    System.out.println("获取当前指定IP的名称:" + id.getHostName());
    // 获取当前指定IP的名称:49.235.99.193
    System.out.println("获取当前本机的IP对象:" + InetAddress.getLocalHost());
    // 获取当前本机的IP对象:DESKTOP-EHHFE8S/192.168.0.101  

UDP操作:
DatagramSocket类:此类表示用于发送和接收数据报数据包的套接字。
    DatagramSocket() :构造数据报套接字并将其绑定到本地主机上的任何可用端口。 
    DatagramSocket(int port) :构造数据报套接字并将其绑定到本地主机上的指定端口。 
    DatagramSocket(int port, InetAddress laddr) :创建一个数据报套接字,绑定到指定的本地地址。 
    receive(DatagramPacket p) :从此套接字接收数据报包。 
    send(DatagramPacket p) :从此套接字发送数据报包。 
DatagramPacket类:该类表示数据报包。 
    DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) :
    构造用于发送长度的分组数据报包length具有偏移 ioffset指定主机上到指定的端口号。  
        byte[] buf:数据包数据
        int offset:数据包数据偏移量
        int length:数据包数据长度
        InetAddress address:目的地地址
        int port:目标端口号
    InetAddress getAddress() :返回该数据报发送或接收数据报的计算机的IP地址。  
    byte[] getData() :返回数据缓冲区。  
    int getLength() :返回要发送的数据的长度或接收到的数据的长度。  
    int getOffset() :返回要发送的数据的偏移量或接收到的数据的偏移量。  
    int getPort() :返回发送数据报的远程主机上的端口号,或从中接收数据报的端口号。 
MulticastSocket类:组播数据报套接字类对发送和接收IP组播数据包很有用。
    void joinGroup(InetAddress mcastaddr) :加入组播组

TCP操作:
Socket类:该类实现客户端套接字(也称为“套接字”)。套接字是两台机器之间通讯的端点。 
ServerSocket类:这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,
    然后可能将结果返回给请求者。 

2:Java实现UDP通信(单播)

  UDP是面向无连接通信协议,这种协议可以单播、组播、群播这3种方式;发送速度快,但是一次性只能发送最多64K大小,而且数据发送也不安全,容易丢失数据。

/**
 * @author Anhui OuYang
 * @version 1.0
 * UDP接收端(先启动)
 **/
public class UDPReceive 
    
    public static void main(String[] args) throws UnknownHostException 
        //此类表示用于发送和接收数据报数据包的套接字。
        DatagramSocket datagramSocket = null;
        try 
            //绑定到10086端口,方便从10086端口接收数据(此时我们编写的是接收端)
            datagramSocket = new DatagramSocket(10086);

            //用来接收消息的包
            byte[] bytes = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length);

            //接收数据
            datagramSocket.receive(datagramPacket);

            byte[] data = datagramPacket.getData();     // 获取数据
            int length = datagramPacket.getLength();    // 获取数据长度
            InetAddress address = datagramPacket.getAddress();  // 接收到哪个ip发来的数据
            int port = datagramPacket.getPort();    // 对方使用哪个端口发送的
            String str = new String(data, 0, length, StandardCharsets.UTF_8);
            System.out.println("接收数据:");
            System.out.println(str);
            System.out.println("从哪个ip发送来的数据:" + address + " 对方使用哪个端口发送数据:" + port);
            //打印消息:
            //  接收数据:
            //  一个简单下消息:
            //  您好朋友
            //  从哪个ip发送来的数据:/192.168.0.101 对方使用哪个端口发送数据8081
         catch (IOException e) 
            throw new RuntimeException(e);
         finally 
            //关闭Socket对象
            if (datagramSocket != null) 
                datagramSocket.close();
            
        
    
/**
 * @author Anhui OuYang
 * @version 1.0
 * UDP发送端
 **/
public class UDPSend 
    
    public static void main(String[] args) throws UnknownHostException 
        //此类表示用于发送和接收数据报数据包的套接字。
        DatagramSocket datagramSocket = null;
        try 
            //获取指定IP地址
            InetAddress id = InetAddress.getByName("192.168.0.101");
            //绑定8081端口,从8081端口发送数据(此时我们编写的是发送端)
            datagramSocket = new DatagramSocket(8081);

            //把要发送的消息打包
            byte[] bytes = "一个简单下消息:\\r\\n您好朋友".getBytes(StandardCharsets.UTF_8);
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, id, 10086);

            //发送消息
            datagramSocket.send(datagramPacket);
         catch (IOException e) 
            throw new RuntimeException(e);
         finally 
            //关闭Socket对象
            if (datagramSocket != null) 
                datagramSocket.close();
            
        
    

3:Java实现UDP通信(组播、广播)

组播实现:
    借助 MulticastSocket 类来实现,这个类是一个(UDP)DatagramSocket,具有加入互联网上其他组播主机的“组”的附加功能。 
    组播组由D类IP地址和标准UDP端口号指定。D类IP地址范围为224.0.0.0至239.255.255.255(含)。地址224.0.0.0是保留的,
    不应该使用。
    一个可以通过首先创建具有所需端口的MulticastSocket来加入多播组,然后调用joinGroup(InetAddress groupAddr)方法:
/**
 * @author Anhui OuYang
 * @version 1.0
 * 组播接收者A
 **/
public class UDPMulticastReceiveA 

    public static void main(String[] args) throws IOException 
        MulticastSocket multicastSocket = null;
        try 
            //创建组播接收者(这里绑定10086端口用来接收数据)
            multicastSocket = new MulticastSocket(10086);

            //将当前本机添加到224.0.0.1的这一组中(这样就可以接收到组发来的数据)
            InetAddress byName = InetAddress.getByName("224.0.0.1");
            multicastSocket.joinGroup(byName);

            //用来接收消息的包
            byte[] bytes = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length);

            //接收数据
            multicastSocket.receive(datagramPacket);

            //打印数据
            byte[] data = datagramPacket.getData();     // 获取组播数据
            int length = datagramPacket.getLength();    // 获取组播数据长度
            InetAddress address = datagramPacket.getAddress();  // 接收到哪个ip发来的组播数据
            int port = datagramPacket.getPort();    // 对方使用哪个端口发送的
            String str = new String(data, 0, length, StandardCharsets.UTF_8);
            System.out.println("接收A:接收组播数据:");
            System.out.println(str);
            System.out.println("从哪个ip发送来的组播数据:" + address + " 对方使用哪个端口发送数据:" + port);
         catch (IOException e) 
            throw new RuntimeException(e);
         finally 
            if (multicastSocket != null) 
                //关闭
                multicastSocket.close();
            
        
    
组播接收者A(UDPMulticastReceiveA)
/**
 * @author Anhui OuYang
 * @version 1.0
 * 组播接收者B
 **/
public class UDPMulticastReceiveB 

    public static void main(String[] args) throws IOException 
        MulticastSocket multicastSocket = null;
        try 
            //创建组播接收者(这里绑定10086端口用来接收数据)
            multicastSocket = new MulticastSocket(10086);

            //将当前本机添加到224.0.0.1的这一组中(这样就可以接收到组发来的数据)
            InetAddress byName = InetAddress.getByName("224.0.0.1");
            multicastSocket.joinGroup(byName);

            //用来接收消息的包
            byte[] bytes = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length);

            //接收数据
            multicastSocket.receive(datagramPacket);

            //打印数据
            byte[] data = datagramPacket.getData();     // 获取组播数据
            int length = datagramPacket.getLength();    // 获取组播数据长度
            InetAddress address = datagramPacket.getAddress();  // 接收到哪个ip发来的组播数据
            int port = datagramPacket.getPort();    // 对方使用哪个端口发送的
            String str = new String(data, 0, length, StandardCharsets.UTF_8);
            System.out.println("接收B:接收组播数据:");
            System.out.println(str);
            System.out.println("从哪个ip发送来的组播数据:" + address + " 对方使用哪个端口发送数据:" + port);
         catch (IOException e) 
            throw new RuntimeException(e);
         finally 
            if (multicastSocket != null) 
                //关闭
                multicastSocket.close();
            
        
    
组播接收者B(UDPMulticastReceiveB)
/**
 * @author Anhui OuYang
 * @version 1.0
 * 组播发送者
 **/
public class UDPMulticastSend 

    public static void main(String[] args) 

        MulticastSocket multicastSocket = null;
        try 
            //创建组播Socket
            multicastSocket = new MulticastSocket();

            //将当前本机添加到224.0.0.1的这一组中(发送数据到这一组)
            // IP地址范围为224.0.0.0(特殊不可用)至239.255.255.255(包含)
            InetAddress groupId = InetAddress.getByName("224.0.0.1");
            multicastSocket.joinGroup(groupId);

            //把要发送的消息打包
            byte[] bytes = "一个简单下消息:\\r\\n您好朋友".getBytes(StandardCharsets.UTF_8);
            DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, groupId, 10086);

            //发送消息
            multicastSocket.send(datagramPacket);
         catch (IOException e) 
            throw new RuntimeException(e);
         finally 
            if (multicastSocket != null) 
                //关闭
                multicastSocket.close();
            
        
    
组播发送者(UDPMulticastSend)

广播实现:广播实现是最简单的,在单播的基础上把发送端的IP改为255.255.255.255,这时候则会对当前局域网内的所有ip发送数据

4:Java实现TCP通信(发送接收应答)

  TCP协议是面向连接的通信协议。速度慢,没有大小限制,数据安全

/**
 * @author Anhui OuYang
 * @version 1.0
 * TCP服务端(先启动服务端)
 **/
public class TCPDemoService 
    public static void main(String[] args) 
        try 
            //TCP服务端(监听10086端口,等待客户端发送数据到这个端口)
            ServerSocket serverSocket = new ServerSocket(10086);
            //接收消息
            Socket accept = serverSocket.accept();

            //服务的获取流,并接收数据(客户端传的是文本)
            System.out.println("开始接收数据!");
            BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
            String str = "";
            while ((str = br.readLine()) != null) 
                System.out.println("打印数据:" + str);
            
            //告知客户端,服务端接收数据完成
            accept.shutdownInput();
            System.out.println("数据接收完成,准备断开连接");

            //发送数据告知客户端,服务端已经处理本次消息
            OutputStream outputStream = accept.getOutputStream();
            byte[] bytes = "服务器处理完成".getBytes(StandardCharsets.UTF_8);
            outputStream.write(bytes, 0, bytes.length);
            //告知客户端,服务端写出的数据完成(注:不这么写会报:java.net.SocketException: Connection reset)
            //因为客户端会一直读服务端返回的数据,此时没有这个方法,则服务端执行close直接关闭了,那么客户端执
            //行inputStream.read()就会出现问题,因为服务端都关闭了,你们客户端执行read方法肯定报错
            accept.shutdownOutput();

            //关闭Socket连接
            serverSocket.close();
         catch (IOException e) 
            throw new RuntimeException(e);
        
    
/**
 * @author Anhui OuYang
 * @version 1.0
 * TCP客户端
 **/
public class TCPDemoClient 
    public static void main(String[] args) 
        try 
            //创建Socket,并且连接服务器127.0.0.0:10086的服务器上
            Socket socket = new Socket("127.0.0.1", 10086);

            //获取网络输出流
            OutputStream outputStream = socket.getOutputStream();

            //发送数据给服务端
            byte[] bytes = "今天真漂亮\\r\\n啦啦啦".getBytes(StandardCharsets.UTF_8);
            outputStream.write(bytes, 0, bytes.length);
            //告知服务器,数据发送结束
            socket.shutdownOutput();

            //等待获取服务器发送过来的处理成功消息
            InputStream inputStream = socket.getInputStream();
            byte[] bytes1 = new byte[1024];
            int s;
            while ((s = inputStream.read(bytes1)) != -1) 
                System.out.print(new String(bytes1, 0, s, StandardCharsets.UTF_8));
            

            //关闭Socket,关闭这个系统会默认先关闭outputStream
            socket.close();
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

5:Java实现TCP通信(文件上传)

/**
 * @author Anhui OuYang
 * @version 1.0
 * 文件上传服务端(先启动)
 **/
public class TCPUploadFileService 
    public static void main(String[] args) 
        try 
            //创建服务端的Socket,并监听指定端口
            ServerSocket serverSocket = new ServerSocket(10086);
            //监听指定端口(等待数据的发来)
            Socket socket = serverSocket.accept();

            //从Socket里获取网络输入流(这个流有用户传来的数据信息)
            BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream());

            //获取服务器的输出流,用来写到服务器的资源文件夹下
            //获取资源路径(后面用来存储文件的)
            URL url = TCPUploadFileClient.class.getResource("/file");
            assert url != null; //断言明确肯定url不为null
            String fileName = UUID.randomUUID() + ".zip";
            FileOutputStream fileOutputStream = new FileOutputStream(url.getFile() + "/" + fileName);
            //包装一层缓冲流
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);

            //写入资源到服务器资源下
            byte[] bytes = new byte[1024];
            int index = 0;
            while ((index = bufferedInputStream.read(bytes)) != -1) 
                bufferedOutputStream.write(bytes, 0, index);
            
            //告知服务端接收文件成功(写出文件信息)(其实下载也是一个样子,只不过是流的信息是颠倒的)
            socket.shutdownInput();
            //写出成功信息
            BufferedWriter bufferedWriter = 
          new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("文件上传成功,上传位置:" + url.getFile() + "/" + fileName); bufferedWriter.flush(); bufferedWriter.newLine(); bufferedWriter.close(); //关闭Socket还得关闭我们自己创建的流 socket.close(); bufferedOutputStream.close(); bufferedWriter.close(); catch (IOException e) throw new RuntimeException(e);
/**
 * @author Anhui OuYang
 * @version 1.0
 * 文件上传客户端(上传文件到客户端)
 **/
public class TCPUploadFileClient 
    public static void main(String[] args) 
        try 
            //创建Socket,并连接到指定的服务器
            Socket socket = new Socket("192.168.0.100", 10086);

            //获取项目下指定文件的路径资源流(file/testUser/jdk.zip需要提前在resources资源目录下定义好)
            //发送jdk.zip给服务端
            InputStream inputStream = TCPUploadFileClient.class
                    .getResourceAsStream("/file/testUser/jdk.zip");
            //获取Socket的网络流(输出)
            OutputStream outputStream = socket.getOutputStream();

            //写出jdk.zip数据发送到客户端
            byte[] bytes = new byte[1024];
            int index = 0;
            while ((index = inputStream.read(bytes)) != -1) 
                outputStream.write(bytes, 0, index);
            
            //告知服务端写出数据结束
            socket.shutdownOutput();

            //接收服务端返回来的信息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("服务端返回:" + bufferedReader.readLine());
            socket.shutdownInput();

            //关闭Socket还得关闭我们自己创建的流
            socket.close();
            inputStream.close();
            bufferedReader.close();
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

  使用Java自带的Socket完成了基本的TCP和UDP的案例,但是企业中大部分都是使用Web方式或者SpringBoot集成的方式完成,后面会详细介绍。

三:WebApp方式实现Socket网络编程

  在这一节将使用普通的WebSocket的方式来完成基本的聊天功能的实现,这里将不详细介绍,具体的在SpringBoot集成的那节介绍;所以这里直接上代码和示例:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>websocket</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- 导入WebSocket坐标(这种是满足javaEE规范)-->
        <!-- 注:具体实现需要导入Application Server依赖,例如Tomcat、Jetty或GlassFish(这里使用Tomcat)-->
        <dependency>
            <groupId>jakarta.websocket</groupId>
            <artifactId>jakarta.websocket-api</artifactId>
            <version>2.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Tomcat依赖坐标 -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-websocket</artifactId>
            <version>9.0.74</version>
        </dependency>
        <!--导入Lombok插件坐标-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <!--Json处理转换坐标-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.29</version>
        </dependency>

        <!--=================== 日志坐标导入 Start ===================-->
        <!--Log4j2自带的日志门面-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!--Log4j2具体的日志实现-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!--导入slf4j日志的门面(最终这个案例的日志实现是log4j2) -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.35</version>
        </dependency>
        <!--为slf4j绑定日志实现 log4j2的适配器 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!--=================== 日志坐标导入 End ===================-->
    </dependencies>

    <build>
        <plugins>
            <!--配置maven编译版本-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source><!--源代码使用的JDK-->
                    <target>1.8</target><!--target需要生成的目标class文件的编译版本-->
                    <encoding>UTF-8</encoding><!--字符集编码,防止中文乱码-->
                    <failOnError>true</failOnError><!--指示即使存在编译错误,构建是否仍将继续-->
                    <failOnWarning>false</failOnWarning><!--指示即使存在编译警告,构建是否仍将继续-->
                    <showDeprecation>false</showDeprecation><!--设置是否显示使用不推荐API的源位置-->
                    <showWarnings>false</showWarnings><!--设为true若要显示编译警告,请执行以下操作-->
                    <meminitial>128M</meminitial><!--编译器使用的初始化内存-->
                    <maxmem>512M</maxmem><!--编译器使用的最大内存-->
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
pom.xml坐标文件
<?xml version="1.0" encoding="UTF-8" ?>
<!--monitorInterval属性值(秒数)为一个非零值来让Log4j每隔指定的秒数来重新读取配置文件,可以用来动态应用Log4j配置-->
<Configuration status="info" monitorInterval="30">
    <!--用来自定义一些变量-->
    <Properties>
        <!--变量定义-->
        <Property name="myPattern" value="%dHH:mm:ss.SSS [%t] %-5level %logger36 - %msg%n"/>
        <!--./logs则会在当前项目的跟目录下创建logs文件夹-->
        <Property name="dir_url">./logs</Property>
    </Properties>
    <!--使用Appender s元素可以将日志事件数据写到各种目标位置-->
    <Appenders>
        <!-- 默认打印到控制台 -->
        <Console name="ConsoleAppend" target="SYSTEM_OUT">
            <!-- 默认打印格式 -->
            <PatternLayout pattern="$myPattern"/>
        </Console>
        <!-- 打印到日志文件上 -->
        <File name="FileAppend" fileName="$dir_url/fileLog.log" bufferedIO="true" immediateFlush="true">
            <PatternLayout>
                <pattern>$myPattern</pattern>
            </PatternLayout>
        </File>
    </Appenders>
    <!--定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <Loggers>
        <!-- 默认打印日志级别为 error -->
        <Root level="INFO">
            <AppenderRef ref="ConsoleAppend"/>
            <AppenderRef ref="FileAppend"/>
        </Root>
    </Loggers>
</Configuration>
log4j2.xml日志配置文件
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Anhui OuYang
 * @version 1.0
 * 注:WebSocket是多例对象,每次一个用户请求都会创建一个新的WebSocket供本次操作
 * WebSocket文件
 **/
@Slf4j
@ServerEndpoint("/test/name")
public class WebSocket 

    //当前客户端连接数
    private static Integer onlineCount = 0;
    //用来记录处于活跃的Socket连接
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
    //用来记录当前连接登录的Session
    private Session session;
    //用来记录当前登录人信息
    private String userMessage;

    /***
     * 创建连接时触发(有客户端请求时打印)
     * @param username 代表地址参数中的name信息,用于接收URL传递的参数(这里代表是谁提交的信息)
     * @param session 当前建立的连接
     */
    @OnOpen
    public void onOpen(@PathParam("name") String username, Session session) 
        log.info("触发 >>> 创建连接触发onOpen()方法设置信息!并设置记录信息", username);
        this.session = session;
        this.userMessage = username;
        clients.put(username, this);    // 当前Socket存起来
        addOnlineCount();               // 当前连接数加1
    

    /***
     * 接收到信息时触发;用于接收客户端发送来的消息,(具体按照业务编写内部代码)
     * 如两个人聊天:应该是看接收到的消息解析后,看看具体发送给谁的,然后转发给另外一个人
     * 比如传来的message可以是如下格式:"to":"jack","message","您好";这时解析后就知道是发送给jack
     * 这时我只需要在 “clients” 集合里找到具体的人转发即可
     * @param session 当前用户连接的Session对象
     * @param message 当前用户发送来的消息(一般为Json数据,好解析)
     */
    @OnMessage
    public void onMessage(Session session, String message) 
        log.info("触发 >>> 接收到信息触发onMessage()方法,处理信息!并返回结果", this.userMessage);
        //解析Json(并获取消息和消息发给谁)
        JSONObject jsonObject = JSONObject.parseObject(message);
        String msgData = jsonObject.getString("toMessage");
        String toName = jsonObject.getString("toName");

        //根据信息名称去集合查询用户信息
        WebSocket webSocket = getClients().get(toName);
        //判断当前的WebSocket是否存在0
        if (webSocket != null) 
            //获取在集合中查询到的Session用户信息,并进行远程调用
            RemoteEndpoint.Async asyncRemote = webSocket.getSession().getAsyncRemote();
            //拼接数据
            String str = "【" + this.userMessage + "】在[" +
                    new SimpleDateFormat("HH:mm:ss").format(new Date()) + "]:" + msgData;
            asyncRemote.sendText(str);
         else 
            session.getAsyncRemote().sendText("当前用户不在线,请稍后联系....");
        
    

    /***
     * 通讯异常时触发
     * @param session 当前用户连接的Session对象
     * @param e 异常信息
     */
    @OnError
    public void onError(Session session, Throwable e) 
        log.info("触发 >>> 通讯异常触发onError()方法,异常信息为:", this.userMessage, e.getMessage());
    

    /***
     * 连接断开时触发
     * @param session 当前用户连接的Session对象
     */
    @OnClose
    public void onClose(Session session) 
        log.info("触发 >>> 连接断开触发onClose()方法,结束连接,关闭: 的连接", this.userMessage, this.userMessage);
        clients.remove(this.userMessage);
        subOnlineCount();
    

    /***
     * 获取当前客户端连接数
     * @return Integer
     */
    public static synchronized Integer getOnlineCount() 
        return onlineCount;
    

    /***
     * 当前客户端连接数+1
     */
    public static synchronized void addOnlineCount() 
        WebSocket.onlineCount++;
    

    /***
     * 当前客户端连接数-1
     */
    public static synchronized void subOnlineCount() 
        WebSocket.onlineCount--;
    

    /***
     * 获取session信息
     * @return Session
     */
    public Session getSession() 
        return session;
    

    /***
     * 获取当前全部活跃的连接
     * @return 活跃集合
     */
    public static synchronized Map<String, WebSocket> getClients() 
        return clients;
    
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>聊天页面</title>
    <style>
        * 
            padding: 0;
            margin: 0;
        

        #app 
            width: 500px;
            height: 500px;
            margin: 10px auto;
            border-radius: 5px;
            border: 1px dashed #333;
            padding: 5px;
        

        .dv 
            border: 1px dashed #f00;
        
    </style>
</head>
<body>
<div id="app">
    <input type="text" id="username" value="" placeholder="请输入上线人">
    <button id="connect" onclick="connection()">连接</button>
    <button id="close" onclick="closeWebSocket()">关闭连接</button>
    <br>
    <input type="text" id="toName" value="" placeholder="消息发送给谁?"><br>
    <input type="text" id="toMessage" value="" placeholder="发送的消息">
    <button id="send" onclick="sendMessage()">发送</button>
    <br>
    <div id="dv"></div>
    <div id="wsMsg">
        <h3>显示Websocket请求信息:</h3>
    </div>
</div>

<script type="text/javascript">
    //定义webSocket对象
    let websocket = null;

    //定义发送函数
    function connection() 
        //定义谁连接Socket
        let username = document.getElementById("username").value;
        //判断当前浏览器是否支持WebSocket
        if (\'WebSocket\' in window) 
            websocket = new WebSocket("ws://192.168.0.106:8080/websocket/test/" + username);
         else if (\'MozWebSocket\' in window) 
            websocket = new MozWebSocket("ws://192.168.0.106:8080/websocket/test/" + username);
         else 
            websocket = new SockJS("192.168.0.106:8080/websocket/test/" + username);
        
        //连接发生错误的回调方法
        websocket.onerror = function () 
            let element = document.createElement("p");
            element.textContent = "WebSocket连接发生错误..."
            document.getElementById("wsMsg").appendChild(element)
        
        //连接成功建立的回调方法
        websocket.onopen = function () 
            let element = document.createElement("p");
            element.textContent = "WebSocket连接成功..." + username
            document.getElementById("wsMsg").appendChild(element)
        
        //接收到消息的回调方法
        websocket.onmessage = function (event) 
            let element = document.createElement("p");
            element.textContent = "数据接收成功..." + event.data
            document.getElementById("wsMsg").appendChild(element)
            //写出到信息栏
            let element1 =<

廖雪峰Java13网络编程-1Socket编程-2TCP编程

在开发网络应用程序的时候,会遇到Socket这个概念。
Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络。
Socket/TCP/部分IP都是由操作系统提供的。不同的编程语言只是提供了对操作系统调用的加单的封装,例如Java提供的几个Socket相关的类就封装了操作系统提供的接口。
为什么需要Socket?
因为仅仅通过IP地址进行通信还不够,同一台计算机同一时间会运行多个网络程序。当计算机收到一个数据包的时候,只有IP地址是没法判断应该发送给哪个应用程序的,所以操作系统抽象出Socket接口。每个应用程序需要对应不同的Socket,可以把Socket简单理解为IP地址+端口号。
端口号是操作系统分配的,是在0-65535之间的数字
<1024是特权端口,需要管理员权限

1024的端口可以有任意应用程序打开
Socket编程模型需要实现服务器端与客户端,因为2个应用程序通信的时候不仅需要对方的IP,还需要知道对方的端口号,所以服务器端必须先固定一个端口号,例如80,客户端在发起请求的时候,会直接请求80端口,同时告知自己的端口,这样双方就可以通过Socket进行通信了。
客户端的编程:

服务器端:

TCPClient.java

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class TCPClient 
    public static void main(String[] args) throws IOException
        InetAddress addr = InetAddress.getLoopbackAddress();
        try(Socket sock = new Socket(addr,9090))
            try(BufferedReader reader = new BufferedReader(new InputStreamReader(sock.getInputStream(),StandardCharsets.UTF_8)))
                try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream(),StandardCharsets.UTF_8)))
                    writer.write("time\\n");
                    writer.flush();
                    String resp = reader.readLine();
                    System.out.println("Response:"+resp);
                
            
        
    

TCPServer.java

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalTime;

public class TCPServer 
    public static void main(String[] args) throws Exception
        ServerSocket ss = new ServerSocket(9090);
        System.out.println("TCP Server ready:");
        Socket sock = ss.accept();
        try(BufferedReader reader = new BufferedReader(new InputStreamReader(sock.getInputStream(),StandardCharsets.UTF_8)))
            try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream(),StandardCharsets.UTF_8)))
                String cmd = reader.readLine();
                if("time".equals(cmd))
                    writer.write(LocalTime.now()+"\\n");
                    writer.flush();
                
            
        
        sock.close();
        ss.close();
    

先运行服务器端,再运行客户端
技术图片

技术图片

总结:

TCP编程模型:

  • 客户端使用Socket(InetAddress,port)打开Socket
  • 服务器用ServerSocket监听端口
  • 服务器用accept接收连接并返回Socket
  • 双方通过Socket打开InputStream/OutputStream读写数据
  • flush()用于强制输出缓冲期

以上是关于Java Socket编程的主要内容,如果未能解决你的问题,请参考以下文章

廖雪峰Java13网络编程-1Socket编程-2TCP编程

Java开发之Socket编程详解

java中的socket编程

Java socket编程API基础

Java Socket编程

基于Socket的java网络编程