最新要闻
- 环球聚焦:讯飞输入法推出苹果 macOS 版,支持 10.15 及以上版本
- 当前快看:江苏女子到山东旅游买到的特产竟是戒尺:自己之前根本没有见过
- 环球速读:80、90后的青春记忆!《街霸》过气了吗?
- iPhone用户被骗子盯上!三招轻松破解
- 北京一车主遇无接触事故被认定负全责 骑车人自己滑倒:网友吵翻
- 百万召回能解决单踏板电门当刹车?特斯拉回应:选择权给大家 误踩会提醒
- 莱州市永安路街道:帮办代办暖心解忧 架起为民服务“连心桥”
- 23岁网红用GPT-4复制自己,每月狂赚3500万 当前播报
- 七孔大豆纤维夏被到手59元:牛奶般丝滑 亲肤透气 今日报
- 何炅录制芒果TV《向往的生活》:手机真我11 Pro+抢镜
- 安卓机皇!三星Galaxy S23 Ultra限量版上市:9488元 全球速读
- 淄博八大局知名麻辣串疑被房东赶走:老板回应双方还在商讨此事 世界视讯
- 厦门英才学校小学部第三套课间操_厦门英才学校小学部-天天热消息
- 就没有《猫和老鼠》还原不了的图!AI被锤爆了
- 热门:中性笔后面的神秘液体是什么?竟然大有讲究!
- 今日讯!德州驴出肉率_德州驴
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
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(File Transfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)服务就是其中两类。 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集成的那节介绍;所以这里直接上代码和示例:
pom.xml坐标文件4.0.0 org.example websocket war 1.0-SNAPSHOT jakarta.websocket jakarta.websocket-api 2.1.0 provided org.apache.tomcat tomcat-websocket 9.0.74 org.projectlombok lombok 1.18.26 com.alibaba.fastjson2 fastjson2 2.0.29 org.apache.logging.log4j log4j-api 2.17.1 org.apache.logging.log4j log4j-core 2.17.1 org.slf4j slf4j-api 1.7.35 org.apache.logging.log4j log4j-slf4j-impl 2.17.1 org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 UTF-8 true false false false 128M 512M
log4j2.xml日志配置文件./logs ${myPattern}
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 Mapclients = new ConcurrentHashMap (); //用来记录当前连接登录的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 getClients() { return clients; }}
聊天页面 <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 = document.createElement("p"); element1.textContent = event.data document.getElementById("dv").appendChild(element1) } //连接关闭的回调方法 websocket.onclose = function () { let element = document.createElement("p"); element.textContent = "WebSocket连接关闭..." document.getElementById("wsMsg").appendChild(element) } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { closeWebSocket(); } } //定义发送ws函数 function sendMessage() { //获取信息 let username = document.getElementById("username").value; let toName = document.getElementById("toName").value let toMessage = document.getElementById("toMessage").value //定义发送的数据 // let sendJson = "{"toName":"+"toName"+"}" let sendJson = `{"toName":"${toName}","toMessage":"${toMessage}"}`; if (websocket != null) { websocket.send(sendJson) //写出到信息栏 let element = document.createElement("p"); element.textContent = "【" + username + "】:" + toMessage document.getElementById("dv").appendChild(element) } } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); }</script>
显示Websocket请求信息:
效果图:
四:使用SpringBoot集成WebSocket原生方式
其实使用这种方式和上面的使用WebApp的方式基本上都是一样,在这里只需要把WebSocket交给Spring容器管理即可,其它的代码和上面的一样,这里我就简单把不一样的代码写出,其实这种我更推荐下面一种方式,使用SpringBoot来处理封装WebSocket的方式。
pom.xml坐标文件代码4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.11 cn.xw SpringBootWebSocket 0.0.1-SNAPSHOT SpringBootWebSocket SpringBootWebSocket org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-log4j2 org.springframework.boot spring-boot-starter-web org.projectlombok lombok 1.18.26 org.springframework.boot spring-boot-starter-websocket com.alibaba.fastjson2 fastjson2 2.0.31 org.apache.maven.plugins maven-compiler-plugin 3.8.1 1.8 UTF-8 true false false false 128M 512M
## 核心代码,把WebSocket交给Spring处理;剩下的代码就和上面的案例一样的,可以任意位置编写/** * @author Anhui OuYang * @version 1.0 **/@SpringBootConfigurationpublic class WebSocketConfig { /*** * 向Spring容器中注入这个ServerEndpointExporter对象,配置WebSocket的服务器端点导出器 * 该实例将会自动扫描Spring容器中所有标注了@ServerEndpoint注解的类,并将其添加到WebSocket服务器中。 * 同时,它还会管理WebSocket服务器的生命周期,如开启和关闭等。 * @return WebSocket的服务器端点 */ @Bean(value = "serverEndpointExporter") public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }}
五:SpringBoot集成WebSocket(重点)
这种方式的集成和上面的几种方式不一样,这种SpringBoot集成的方式是对其作了封装,使我们有更多的灵活性,比如可以编写拦截器等等操作;还需要导入指定的坐标,如下:
org.springframework.boot spring-boot-starter-websocket
1:WebSocket配置类
WebSocketHandlerRegistry:它是Spring Framework中用于注册并管理WebSocket处理程序的类。它是WebSocketConfigurer接口的一部分, 它定义了一个方法registerWebSocketHandlers(WebSocketHandlerRegistry registry), 该方法用于注册和配置WebSocket处理程序实例。 WebSocketHandlerRegistry提供了一些方法,可以用于注册WebSocket处理程序、设置跨域访问规则,设置拦截器等。其主要方法如下: ①:addHandler(WebSocketHandler handler, String... paths):注册WebSocket处理程序,并指定处理程序可访问的路径; ②:setAllowedOrigins(String... origins):设置允许跨域访问的域名列表; ③:addInterceptors(HandshakeInterceptor... interceptors):添加WebSocket握手拦截器; ④:setHandshakeHandler(HandshakeHandler handshakeHandler):设置WebSocket握手处理程序; ⑤:setTaskScheduler(TaskScheduler taskScheduler):设置任务调度程序。(注:需要SpringBoot 2.7.11版本以上)通过WebSocketHandlerRegistry,开发人员可以灵活地配置WebSocket服务器,以满足特定的业务需求和安全要求。
@Configurable //代表当前是一个配置类@EnableWebSocket // 开启WebSocket的自动配置public class WebSocketConfig implements WebSocketConfigurer { /*** * registerWebSocketHandlers方法用于注册WebSocket处理程序 * @param registry 配置信息类 */ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(null, "/websocket/**") // 注册WebSocket处理程序 .setAllowedOrigins("*") // 设置允许跨域访问 .addInterceptors(null) // 添加WebSocket握手拦截器(后面需要实现) .setHandshakeHandler(null); // 设置WebSocket握手处理程序(后面需要实现) }}
2:WebSocket拦截器
其实拦截器有两种,具体看业务上的选择,这里我选择Http方式的拦截器
HandshakeInterceptor接口分别有两个实现类:HttpSessionHandshakeInterceptor:是基于HTTP会话的WebSocket握手拦截器。在握手之前,它会基于当前HTTP请求的会话信息来添加 WebSocket 握手的请求头; 在握手之后,它会通过检查握手请求头来确定是否要创建或关闭会话以及报告任何错误。 WebSocketHandshakeInterceptor:是基于WebSocket协议的握手拦截器。在握手之前,它会检查WebSocket握手请求和响应头,并在需要时添加或删除必要的消息头; 在握手之后,它会通过检查握手请求和响应标头来确保协议交换已成功,如果不成功,它会关闭WebSocket连接并报告任何错误。
/** * WebSocket握手拦截器,检查握手请求和响应,对WebSocketHandler传递属性,用于区别WebSocket * * @author Anhui OuYang * @version 1.0 **/@Slf4jpublic class MyWebSocketInterceptorextends HttpSessionHandshakeInterceptor { /*** * 握手之前被调用 * @param request 请求信息 * @param response 响应信息 * @param wsHandler 用于处理WebSocket通信过程中的各种事件和消息 * @param attributes 如果需要,可以使用setAttribute方法添加属性,这些属性可以在后续处理中使用 * @return 返回true表示继续握手,或者返回false以终止握手 * @throws Exception 异常信息 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Mapattributes) throws Exception { log.info("请求被拦截器拦截,当前请求地址为:{}", request.getURI()); //在拦截器解析信息,并设置到 attributes 中,后续程序可以在这里面取数据 // 示例请求地址:http://localhost:8080/websocket/tom 因为这个restFul风格的地址,所以那个tom我需要拿到 String path = request.getURI().getPath(); String name = path.substring(path.lastIndexOf("/") + 1); attributes.put("loginName", name); log.info("请求握手成功,当前的登录人为:{}", attributes.get("loginName")); return super.beforeHandshake(request, response, wsHandler, attributes); } /*** * 握手成功之后或者失败之后被调用;可以利用这个方法去清理任何未完成的状态并记录异常。 */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { log.info("请求被拦截器拦截放行。。。"); super.afterHandshake(request, response, wsHandler, ex); }}
3:WebSocket消息处理类
继承:TextWebSocketHandler或者继承BinaryWebSocketHandler 是Spring WebSocket框架为我们提供的一个处理WebSocket消息的默认处理器,它可以方便地处理文本消息。 ①:继承TextWebSocketHandler: 我们需要重写handleTextMessage方法,以便在处理到达的文本消息时调用自己的处理程序。但除了这个还有好多方法 ②:继承BinaryWebSocketHandler: 可以处理来自非文本类型的消息以TextWebSocketHandler来说(包含如下方法): ①:handleTextMessage():处理WebSocket文本消息。 ②:afterConnectionEstablished():在WebSocket连接建立后调用。 ③:afterConnectionClosed():在WebSocket连接关闭后调用。 ④:handleTransportError():在WebSocket传输错误时调用。 ⑤:supportsPartialMessages():返回是否支持部分消息。以BinaryWebSocketHandler来说(包含如下方法): ①:handleBinaryMessage():处理WebSocket二进制消息。 ②:afterConnectionClosed():在WebSocket连接关闭后调用。 ③:afterConnectionEstablished():在WebSocket连接建立后调用。 ④:handleTransportError():在WebSocket传输错误时调用。 ⑤:supportsPartialMessages():返回是否支持部分消息。
/** * WebSocket消息处理,这里以TextWebSocketHandler文本的处理方式 * * @author Anhui OuYang * @version 1.0 **/@Slf4j@Componentpublic class SimpleWebSocketMessageHandler extends TextWebSocketHandler { //当前客户端连接数 private static Integer onlineCount = 0; //用来记录处于活跃的Socket连接 private static MapallClients = new HashMap<>(); /*** * 在WebSocket连接建立后调用 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { log.info("afterConnectionEstablished() 方法执行,在WebSocket连接建立后调用...."); //保存当前连接Socket的信息 String name = String.valueOf(session.getAttributes().get("loginName")); if (name != null) { allClients.put(name, session); // 把当前用户存起来 addOnlineCount(); // 当前连接数加1 } } /*** * 处理WebSocket文本消息(若处理二进制非文本数据则使用handleMessage()方法或者继承BinaryWebSocketHandler类) */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { log.info("handleTextMessage() 方法执行,处理WebSocket文本消息.... 调用人:{}" , session.getAttributes().get("loginName")); //解析Json(并获取消息和消息发给谁) JSONObject jsonObject = JSONObject.parseObject(message.getPayload()); String msgData = jsonObject.getString("toMessage"); String toName = jsonObject.getString("toName"); //根据信息名称去集合查询用户信息 WebSocketSession webSocketSession = getAllClients().get(toName); //判断当前的WebSocket是否存在 if (webSocketSession != null && webSocketSession.isOpen()) { //拼接发送的消息和包装消息对象 String str = "【" + session.getAttributes().get("loginName") + "】在[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "]:" + msgData; TextMessage textMessage = new TextMessage(str); //获取在集合中查询到的WebSocketSession用户信息,并进行远程调用 webSocketSession.sendMessage(textMessage); } } /*** * 在WebSocket传输错误时调用 */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.info("handleTransportError() 方法执行,在WebSocket传输错误时调用...."); super.handleTransportError(session, exception); } /*** * 在WebSocket连接关闭后调用 */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { log.info("afterConnectionClosed() 方法执行,在WebSocket连接关闭后调用...."); super.afterConnectionClosed(session, status); } /*** * 返回是否支持部分消息 */ @Override public boolean supportsPartialMessages() { log.info("supportsPartialMessages() 方法执行,方法返回是否支持部分消息...."); return super.supportsPartialMessages(); } /*** * 获取当前客户端连接数 * @return Integer */ public static synchronized Integer getOnlineCount() { return onlineCount; } /*** * 当前客户端连接数+1 */ public static synchronized void addOnlineCount() { SimpleWebSocketMessageHandler.onlineCount++; } /*** * 当前客户端连接数-1 */ public static synchronized void subOnlineCount() { SimpleWebSocketMessageHandler.onlineCount--; } /*** * 获取当前全部活跃的连接 * @return 活跃集合 */ public static Map getAllClients() { return allClients; }}
4:最后一步(修改配置和前端页面)
@SpringBootConfiguration //代表当前是一个配置类@EnableWebSocket // 开启WebSocket的自动配置public class WebSocketConfig implements WebSocketConfigurer { //注入对象 private final SimpleWebSocketMessageHandler simpleWebSocketMessageHandler; @Autowired public WebSocketConfig(SimpleWebSocketMessageHandler simpleWebSocketMessageHandler) { this.simpleWebSocketMessageHandler = simpleWebSocketMessageHandler; } /*** * registerWebSocketHandlers方法用于注册WebSocket处理程序 * @param registry 配置信息类 */ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(simpleWebSocketMessageHandler, "/websocket/**") // 注册WebSocket处理程序 .setAllowedOrigins("*") // 设置允许跨域访问 .addInterceptors(new MyWebSocketInterceptor()) // 添加WebSocket握手拦截器(后面需要实现) .setHandshakeHandler(null); // 设置WebSocket握手处理程序(后面需要实现) }}
前端页面(还是和之前的一样,前端页面可以放在任意地方,到时候可以运行就行)聊天页面 <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://localhost:8080/websocket/" + username); } else if ("MozWebSocket" in window) { websocket = new MozWebSocket("ws://localhost:8080/websocket/" + username); } else { websocket = new SockJS("localhost:8080/websocket/" + 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 = document.createElement("p"); element1.textContent = event.data document.getElementById("dv").appendChild(element1) } //连接关闭的回调方法 websocket.onclose = function () { let element = document.createElement("p"); element.textContent = "WebSocket连接关闭..." document.getElementById("wsMsg").appendChild(element) } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { closeWebSocket(); } } //定义发送ws函数 function sendMessage() { //获取信息 let username = document.getElementById("username").value; let toName = document.getElementById("toName").value let toMessage = document.getElementById("toMessage").value //定义发送的数据 // let sendJson = "{"toName":"+"toName"+"}" let sendJson = `{"toName":"${toName}","toMessage":"${toMessage}"}`; if (websocket != null) { websocket.send(sendJson) //写出到信息栏 let element = document.createElement("p"); element.textContent = "【" + username + "】:" + toMessage document.getElementById("dv").appendChild(element) } } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); }</script>
显示Websocket请求信息:
具体的运行效果和上面的WebApp方式的运行一样的,一样的前端,只不过后端实现不同。
关键词:
-
Ubuntu下通过Wine安装LTSpice 17.1.8_当前讯息
LTSpice是常用的电路模拟软件,但是只有Windows版本和Mac版本,在Linux下需要用Wine运行 以下说明如何在Ubunt
来源: -
环球聚焦:讯飞输入法推出苹果 macOS 版,支持 10.15 及以上版本
IT之家5月13日消息,据讯飞输入法官方消息,讯飞输入法首个macOS版本现已发布,支持macOS10 15及以上更高系
来源: Java Socket编程|环球聚看点
Ubuntu下通过Wine安装LTSpice 17.1.8_当前讯息
环球聚焦:讯飞输入法推出苹果 macOS 版,支持 10.15 及以上版本
当前快看:江苏女子到山东旅游买到的特产竟是戒尺:自己之前根本没有见过
环球速读:80、90后的青春记忆!《街霸》过气了吗?
iPhone用户被骗子盯上!三招轻松破解
北京一车主遇无接触事故被认定负全责 骑车人自己滑倒:网友吵翻
百万召回能解决单踏板电门当刹车?特斯拉回应:选择权给大家 误踩会提醒
莱州市永安路街道:帮办代办暖心解忧 架起为民服务“连心桥”
23岁网红用GPT-4复制自己,每月狂赚3500万 当前播报
Prompt learning 教学[案例篇]:文生文案例设定汇总,你可以扮演任意角色进行专业分析-天天即时
七孔大豆纤维夏被到手59元:牛奶般丝滑 亲肤透气 今日报
何炅录制芒果TV《向往的生活》:手机真我11 Pro+抢镜
安卓机皇!三星Galaxy S23 Ultra限量版上市:9488元 全球速读
淄博八大局知名麻辣串疑被房东赶走:老板回应双方还在商讨此事 世界视讯
厦门英才学校小学部第三套课间操_厦门英才学校小学部-天天热消息
今日快讯:算命奇想
就没有《猫和老鼠》还原不了的图!AI被锤爆了
热门:中性笔后面的神秘液体是什么?竟然大有讲究!
今日讯!德州驴出肉率_德州驴
小米13 Pro被低估了!雷军力荐:数码发烧友就选它|观点
通讯!00后女生旅游不忘给新手机开光 网友:你是懂开光的
焦点资讯:《暗黑4》遇上DLSS 3:最低帧猛增50%
迁移到 Gradle 7.x 使用 Version Catalogs 管理依赖
焦点热议:线段树
【LeetCode剑指offer#04】包含min函数的栈、栈的压入、弹出序列(辅助栈的应用)
环球速读:51岁已被游客喊了十多年谭爷爷!熊猫饲养员谭金淘“出圈”
全球快讯:17岁少年骑共享单车52天5000公里!成都-拉萨-新疆 穿越无人区
当前快看:12岁男孩玩游戏一个月花掉10多万!家长申请退款却遭拒绝
聊一聊:iOS 16.5 RC准正式版推送!iOS 16系统更新要绝唱了
使用go-cqhttp搭建qq机器人
每天坐高铁上下班是什么体验?一个月2400元
拒绝爆显存!RTX 4060 Ti 16GB曝新料:功耗增加5W|环球观点
WSL Ubuntu 安装 minikube 世界热消息
工程粉墙合同范本(实用4篇)
环球新资讯:长征九号、长征十号火箭发动机好消息!200吨、载人可复用
男子淄博吃烧烤吐槽扇贝肉指甲大 网友:应该去青岛|世界最新
再也不用担心爱车被破坏!理想官方详解哨兵模式:三大优势|环球观热点
要闻速递:昔日安卓手机王者!HTC U23 Pro官宣:搭载骁龙7芯片
当前讯息:观战台:曼城阿森纳继续隔空对话 巴萨赢球即夺冠
精选!2022年全球电动车销量排行榜前十名出炉:中国霸气占7席
这届年轻人“断亲”或已成常态 90后/00后几乎都不走亲戚:原因无奈
瑞吉外卖day4
看点:杲杲冬日光明暖真可爱什么意思-杲杲冬日光明暖真可爱
男子报案在香港中环码头遭遇抢劫 其周杰伦演唱会门票被抢走
世嘉公布《魔界战记7》角色“彼岸绝胜斋”中文版介绍影像 为最恶毒剑士
人气手游《赛马娘》樱花进王1/7比例手办现已开定 将于10月发售
《爱恋冰果室》即将登陆XboxS 为恋爱经营模拟游戏
【世界新视野】4月新能源销量排名:最亮眼的 竟然不是比亚迪
当前速读:为什么面粉没有以前的香?加了添加剂安全吗?
焦点信息:员工病假15年起诉IBM:认为工资无法赶上通胀
舞台剧《全职猎人》在东京银河剧场举行公开彩排 讲述小杰等人故事
《圣斗士星矢》真人电影在北美上映 烂番茄新鲜度已降至17%
《塞尔达传说:王国之泪》官方推出系列周边 包括T恤与水杯等产品
小岛秀夫纪录片即将上线翠贝卡电影节首映 时长约为90分钟
《SD高达激斗同盟》新DLC水星的魔女现已发售 国区定价为48元
90后跆拳道女运动员娜娜摆摊卖水果 积极向上态度引发网友赞叹
【世界新视野】Python学习之五_字符串处理生成查询SQL
spring框架_@AutoWiredAnnotationBeanPostProcessor执行分析 今日聚焦
中国恒大公告:许家印成被执行人,涉及金额超 60 亿元-世界热门
某考研机构欲花百万请孟羽童代言 网友热议:赢麻了|当前快报
男子剪鼻毛致鼻腔感染住院12天 医生提醒:非常危险|每日信息
全球看点:将乔峰、郭靖等改写成大学生!金庸生前诉江南案终审宣判:获赔188万
Hyper Demon拆解 当前速看
新疆维吾尔自治区阿勒泰市2023-05-13 12:01发布暴雨蓝色预警 全球热闻
女子辞年薪50万工作照顾瘫痪奶奶:只想陪她走完最后一程-全球微头条
世界快播:国人最爱买啥车?2023新车购买意向研报出炉:比亚迪夺冠
券后9块9!汤臣倍健维生素D钙片大促:手慢真的无
环球新动态:苏享茂家属称收到翟欣欣660万还款,后者被判退还千万财物
热门:恒大汽车公告:剥离地产业务因资金不足已暂缓生产恒驰5
世界快资讯丨理想城市NOA测试画面曝光 马斯克:看起来有点眼熟
世界热门:AI出来后第一个失业的是孙燕姿?歌迷齐呼:等你的演唱会
天天速讯:夜莺初探四·mtail插件采集日志指标
《首次公开募股(IPO)企业品牌建设指南》标准立项暨“IPO企业品牌辅导专项行动”正式启动_动态
撑起国产芯片!华为继续摸索半导体核心技术:新专利带来更高效封装 天天微头条
伐木累暗示速激12
怎样查询个人养老保险缴纳的情况信息 怎样查询个人养老保险缴纳的情况|信息
当前资讯!男童酒店客房误食用过的安全套 家长与酒店吵翻:网友直呼太巧合
90后宝爸拽脚倒提1岁女儿摸金鱼 妈妈称没危险:网友吐槽为啥要摸 世界微资讯
今日热议:广西:5月13日至15日开展高考志愿填报演练
世界快播:分布式系统常见问题
百度手机曝光:后置双摄+4G网络 世界快资讯
骁龙888再就业!荣耀新平板入网:13英寸超大LCD屏_全球速看
德普拿下超级香水合约
速递!AI出来后第一个失业的是孙燕姿:为什么说她的音色不受法律保护
天天最资讯丨中国地形地貌最全的省份:你可能想不到
专家称睡满8小时是误区:张朝阳曾称四小时足够 睡多浪费
项目一个不留!OPPO放弃ZEKU自研芯片业务:多大代价都是最小代价 世界快资讯
法网公布奖金分配:总额提升12.3%,低排位选手获利,冠军230万欧-世界今日报
全球今热点:地位远超iPhone!苹果Reality Pro头显12月大规模量产:售价超2万
环球快播:小白也能辨别!买独显轻薄本必须要看的四大规格
马斯克:当Linda Yaccarino准备好时,我们会在Twitter Spaces进行实时音频对话,欢迎提问任何事情。-每日报道
有赞一面:亿级用户DAU日活统计,有几种方案?-每日速递
每日快播:gazebo+rviz 仿真
OPPO放弃自研芯片!马里亚纳X/Y官网页面悄然404_环球微速讯
天天快报!国外玩家质疑《塞尔达传说:王国之泪》涉嫌抄袭:照搬游戏机制
二次圆满成功!中国可重复火箭发动机传喜讯 今头条
在美国加州 我亲眼见到了Google对AI的孤注一掷_每日热闻
性能秒RTX 4060?AMD新显卡性能实测 光追是短板 今日报
庄河市气象局发布雷电黄色预警【III级/较重】【2023-05-13】