基于Spring-boot-websocket的聊天应用开发总结
目录
1.概述
1.1 Websocket
1.2 STOMP
1.3 源码
2.Springboot集成WS
2.1 添加依赖
2.2 ws配置
2.2.1 WebSocketMessageBrokerConfigurer
2.2.2 ChatController
2.2.3 ChatInRoomController
2.2.4 ChatToUserController
2.3 前端聊天配置
2.3.1 index.html和main.js
2.3.2 chatInRoom.html和chatInRoom.js
2.3.3 chatToUser.html和chatToUser.js
2.4 测试
2.4.1 基础的发布订阅测试
2.4.2 群聊测试
2.4.3 私聊测试
3 参考总结
最近在研究通过spring-boot-websocket开发简单的聊天应用,以下对这几天做一下总结。
关于WebRTC原理我主要是通过《WebRTC音视频实时互动技术原理、实战与源码分析》这本书了解底层的框架和实现思路,电子版资料可以私聊我。
1.概述
1.1 Websocket
WebSocket 连接允许客户端和服务器进行全双向通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
如果仅使用WebSocket完成群聊、私聊功能时需要自己管理session信息,但通过STOMP协议时,Spring已经封装好,开发者只需要关注自己的主题、订阅关系即可。
1.2 STOMP
STOMP即“面向消息的简单文本协议”,提供了能够协作的报文格式,以至于 STOMP 客户端可以与任何 STOMP 消息代理(Brokers)进行通信,从而为多语言,多平台和 Brokers 集群提供简单且普遍的消息协作。
STOMP 协议可以建立在WebSocket 之上,也可以建立在其他应用层协议之上。通过 Websocket建立 STOMP 连接,也就是说在 Websocket 连接的基础上再建立 STOMP 连接。最终实现如上图所示,这一点可以在代码中有一个良好的体现。
主要包含如下几个协议事务:
- CONNECT:启动与服务器的流或 TCP 连接
- SEND:发送消息
- SUBSCRIBE:订阅主题
- UNSUBSCRIBE:取消订阅
- BEGIN:启动事务
- COMMIT:提交事务
- ABORT:回滚事务
- ACK:确认来自订阅的消息的消费
- NACK:告诉服务器客户端没有消费该消息
- DISCONNECT:断开连接
1.3 源码
git地址:https://github.com/BAStriver/spring-boot-websocket-chat-app
下载路径:https://download.csdn.net/download/BAStriver/88711460
2.Springboot集成WS
2.1 添加依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId><version>6.0.7</version></dependency>
</dependencies>
2.2 ws配置
2.2.1 WebSocketMessageBrokerConfigurer
这里主要是配置STOMP协议端点、消息代理。
并且设置了前端发布消息的前缀为/app,和消息代理的前缀/topic(@SendTo中为/topic/*)。
// register STOMP endpoints
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws") // this is the endpoint which should be set in SockJS client.setAllowedOriginPatterns("*") // allow cross-domain request.withSockJS(); // use SockJS protocol
}// register message broker
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {// while sending messages in front end, the path should add the prefix as /appregistry.setApplicationDestinationPrefixes("/app");// enable and set the prefixes of broker paths, like /topic/public// without this prefix, it will block those sent messagesregistry.enableSimpleBroker("/topic", "/user");// while sending messages to user in front end, the path should add the prefix as /user// default is /userregistry.setUserDestinationPrefix("/user");
}
2.2.2 ChatController
以下是基础的控制器,通过sendMessage()发布消息,通过addUser()把订阅者加入到session管理,并最终返回到订阅路径/topic/public。
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage
) {return chatMessage;
}@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());return chatMessage;
}
经过上面的方法可以实现发布订阅模式。
值得注意的是,如果没有配置@SendTo,则消息会默认返回到@MessageMapping的路径给订阅者。
2.2.3 ChatInRoomController
这个主要是实现群聊。
@MessageMapping("/chat/{roomId}")
@SendTo("/topic/chat/{roomId}") // if not add @SendTo, then by default will send to the path /topic/chat/{roomId}
public ChatMessage sendMessage(@DestinationVariable String roomId, ChatMessage message) {log.info("roomId: {}", roomId);return message;
}// if need the {roomId} in @SendTo,
// then should add {roomId} in @MessageMapping and sent roomId from front end.
// otherwise, it could not resolve placeholder 'roomId' in value "/topic/chat/{roomId} of @SendTo
@MessageMapping("/chat.addUserToRoom/{roomId}")
@SendTo("/topic/chat/{roomId}")
public ChatMessage addUser(@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());return chatMessage;
}
值得注意的是,如果@SendTo需要{roomId}这个参数,那么在@MessageMapping()中也需要传入{roomId}。
2.2.4 ChatToUserController
这个主要实现单独发布消息到指定的订阅者。
@MessageMapping("/chatToUser/{userId}")
@SendTo(value = "/topic/chatToUser/{userId}")
public ChatMessage sendMessage(@DestinationVariable String userId, ChatMessage message,SimpMessageHeaderAccessor headerAccessor) {log.info("send to the userId: {}", userId);log.info("message: {}", message);// Set<StompAuthenticatedUser> collect = simpUserRegistry.getUsers().stream()
// .map(simpUser -> StompAuthenticatedUser.class.cast(simpUser.getPrincipal()))
// .collect(Collectors.toSet());
// collect.forEach(user -> {
// if(user.getNickName().equals(userId)) {
// simpMessagingTemplate.convertAndSendToUser(userId, "/chatToUser/"+userId, message);
// }
// });return message;
}@MessageMapping("/chat.helloUser/{userId}")
@SendTo("/user/chat/{userId}")
public ChatMessage helloUser(@DestinationVariable String userId,@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());headerAccessor.getSessionAttributes().put("userid", userId);// use the tool to send the message to public topic directly, without @MessageMapping// simpMessagingTemplate.convertAndSend("/user/chat/" + userId, chatMessage);return chatMessage;
}//@MessageMapping("/chat.sendMessage")
@GetMapping("/testSendMessage")
public void testSendMessage(ChatMessage message) {// use the tool to send the message to public topic directly, without @MessageMappingsimpMessagingTemplate.convertAndSend("/topic/public", message);
}
值得注意的是,这里的@MesssageMapping()不要和前面的重复了。
同样的,也可以通过如下的代码实现发布消息。
simpMessagingTemplate.convertAndSend("/user/chat/" + userId, chatMessage);
其实这个部分和#2.2.3同理,不同的是私聊其实可以用@SendToUser。
2.3 前端聊天配置
SockJS 是一个浏览器的 JavaScript库,它提供了一个类似于网络的对象,SockJS 提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS 的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持 WebSocket,会自动降为轮询的方式。如果你使用 Java 做服务端,同时又恰好使用 Spring Framework 作为框架,那么推荐使用SockJS。
2.3.1 index.html和main.js
对应#2.2.2的前端页面和脚本。
这里初始化一个sockjs实例,其中的/ws指定了#2.2.1的STOMP端点。
function connect(event) {username = document.querySelector('#name').value.trim();if(username) {usernamePage.classList.add('hidden');chatPage.classList.remove('hidden');const header = {"User-ID": new Date().getTime().toString(),"User-Name": username};var socket = new SockJS('/ws'); // set the STOMP endpointstompClient = Stomp.over(socket);stompClient.connect(header, onConnected, onError);}event.preventDefault();
}
当客户端和服务Connected之后,开始订阅/topic/public的消息以及设置send()的消息发布路径。
function onConnected() {// Subscribe to the Public TopicstompClient.subscribe('/topic/public', onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.addUser", // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if(messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
值得注意的是,这里send()的时候要记得加/app作为前缀。
2.3.2 chatInRoom.html和chatInRoom.js
对应#2.2.3的前端页面和脚本。
初始化sockjs实例和上面的一样,但要注意的是Connected之后的订阅和发布路径加上了{room}作为聊天室的id。
function onConnected() {// Subscribe the message of the {room}stompClient.subscribe('/topic/chat/'+room, onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.addUserToRoom/"+room, // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if(messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chat/"+room, {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
2.3.3 chatToUser.html和chatToUser.js
对应#2.2.3的前端页面和脚本。
初始化sockjs实例和上面的一样,但要注意的是Connected之后的订阅和发布路径加上了{username}和{userid}作为私聊对象id。
function onConnected() {console.log('username: ', username);console.log('userid: ', userid);// Subscribe the message with {userid}stompClient.subscribe('/user/chat/' + username, onMessageReceived);stompClient.subscribe('/topic/chatToUser/' + username, onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.helloUser/" + username, // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if (messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chatToUser/" + userid, {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
2.4 测试
2.4.1 基础的发布订阅测试
这里测试的#2.3.1的部分。
首先是登录界面,进入:http://localhost:8080/index.html
打开两个index页面,然后输入username之后实现聊天。
第一个index.html登入BAS用户,第二个页面登入BAS55。
2.4.2 群聊测试
这里测试的#2.3.2的部分。
首先是登录界面,进入:http://localhost:8080/chatInRoom.html
第一个index.html登入BAS用户(Room: 12345),
第二个页面登入BAS55(Room: 12345),
第三个页面登入BAS10(Room: 123),BAS10单独在一个房间。
2.4.3 私聊测试
这里测试的#2.3.3的部分。
首先是登录界面,进入:http://localhost:8080/chatToUser.html
第一个index.html登入BAS用户(Chat To: BAS5),
第二个页面登入BAS55(Chat To: BAS),
第三个页面登入BAS10(Chat To: BAS9)。
3 参考总结
以下是开发过程中参考并且觉得挺有帮助的资料:
SpringBoot——整合WebSocket(STOMP协议) - 简书
Spring Boot系列 WebSocket集成简单消息代理_websocketmessagebrokerconfigurer-CSDN博客
WebSocket的那些事(4-Spring中的STOMP支持详解)_simpuserregistry 为空-CSDN博客
注:
1.关于@MessageMapping()的使用可以参考:Spring Boot中的@MessageMapping注解:原理及使用-CSDN博客
2.关于AbstractWebSocketHandler的使用可以参考:WebSocket基本概念及在Spring Boot中的使用 - 知乎
3.关于@SendTo()和@SendToUser()的区别和使用可以参考:在Spring WebSocket中使用@SendTo和@SendToUser进行消息路由 - 实时互动网
Spring-messaging (STOMP) @SendTo 与 @SendToUser的区别-CSDN博客
相关文章:
基于Spring-boot-websocket的聊天应用开发总结
目录 1.概述 1.1 Websocket 1.2 STOMP 1.3 源码 2.Springboot集成WS 2.1 添加依赖 2.2 ws配置 2.2.1 WebSocketMessageBrokerConfigurer 2.2.2 ChatController 2.2.3 ChatInRoomController 2.2.4 ChatToUserController 2.3 前端聊天配置 2.3.1 index.html和main.j…...
2023年度总结 - 职业生涯第一个十年
2023年只剩下最后一周,又到了一年一度该做年末总结的时候了。 回想起去年,还有人专门建立了一个关于年度总结文章汇总的仓库。读了很多篇别人写的,给了我很多的触动和感想。这里的每篇文章都是关于某个人这一整年的生活和工作的轨迹啊。即使你…...
setup 语法糖
只有vue3.2以上版本可以使用 优点: 更少的样板内容,更简洁的代码 能够使用纯 Typescript 声明props 和抛出事件 更好的运行时性能 更好的IDE类型推断性能 在sciprt标识上加上setup 顶层绑定都可以使用 不需要return ,可以直接使用 使用组件…...
Javaweb之Mybatis的基础操作的详细解析
1. Mybatis基础操作 学习完mybatis入门后,我们继续学习mybatis基础操作。 1.1 需求 需求说明 通过分析以上的页面原型和需求,我们确定了功能列表: 查询 根据主键ID查询 条件查询 新增 更新 删除 根据主键ID删除 根据主键ID批量删除 …...
知名开发者社区Stack Overflow发布《2023 年开发者调查报告》
Stack Overflow成立于2008年,最知名的是它的公共问答平台,每月有超过 1 亿人访问该平台来提问、学习和分享技术知识。是世界上最受欢迎的开发者社区之一。每年都会发布一份关于开发者的调查报告,来了解不断变化的开发人员现状、正在兴起或衰落…...
vue element plus Form 表单
表单包含 输入框, 单选框, 下拉选择, 多选框 等用户输入的组件。 使用表单,您可以收集、验证和提交数据。 TIP Form 组件已经从 2. x 的 Float 布局升级为 Flex 布局。 典型表单# 最基础的表单包括各种输入表单项,比如input、select、radio、checkbo…...
zmq_connect和zmq_poll
文章内容: 介绍函数zmq_connect和zmq_poll的使用 zmq_connect zmq_connect函数是ZeroMQ库中的一个函数,用于在C语言中创建一个与指定地址的ZeroMQ套接字的连接。该函数的原型如下: int zmq_connect(void *socket, const char *endpoint);其…...
TinyLog iOS v3.0接入文档
1.背景 为在线教育部提供高效、安全、易用的日志组件。 2.功能介绍 2.1 日志格式化 目前输出的日志格式如下: 日志级别/[YYYY-MM-DD HH:MM:SS MS] TinyLog-Tag: |线程| 代码文件名:行数|函数名|日志输出内容触发flush到文件的时机: 每15分钟定时触发…...
react-native 配置@符号绝对路径配置和绝对路径没有提示的问题
这里需要用到vscode的包 yarn add babel-plugin-module-resolver 找到根目录里的babel.config.js 在页面添加plugins配置 直接替换 module.exports {presets: [module:metro-react-native-babel-preset],plugins: [[module-resolver,{root: [./src],alias: {/utils: ./src/…...
element的Table表格组件树形数据与懒加载简单使用
目录 1. 代码实现2. 效果图3. 解决新增、删除、修改之后树节点不刷新问题。([参考文章](https://blog.csdn.net/weixin_41549971/article/details/135504471)) 1. 代码实现 <template><div><!-- lazy 是否懒加载子节点数据 --><!-…...
游戏、设计选什么内存条?光威龙武系列DDR5量大管饱
如果你是一位PC玩家或者创作者,日常工作娱乐中,确实少不了大容量高频内存的支持,这样可以获得更高的工作效率,光威龙武系列DDR5内存条无疑是理想之选。它可以为计算机提供强劲的性能表现和稳定的运行体验,让我们畅玩游…...
linux磁盘清理_docker/overlay2爆满
问题:无意间发现linux服务器登陆有问题,使用df命令发现目录满了。 1. 确定哪里占用了大量内存。 cd / du -sh * | sort -rh经过一段时间后,显示如下: // 474G home // 230G var // 40G usr // 10G snap // --- 根据实际情…...
Redis过期清理策略和内存淘汰机制
目录 Redis过期清理策略Redis内存淘汰机制 Redis过期清理策略 Redis 通过设置键的过期时间来实现自动删除过期键。当键的过期时间到达时,Redis 会自动将该键删除。Redis 过期清理策略主要有以下两种: 惰性删除:Redis 在获取键时会检查键是否…...
2_并发编程同步锁(synchronized)
并发编程带来的安全性同步锁(synchronized) 1.他的背景 当多个线程同时访问,公共共享资源的时候,这时候就会出现线程安全,代码如: public class AtomicDemo {int i0;//排他锁、互斥锁public void incr(){ //synchronizedi; …...
Python 常用模块pickle
Python 常用模块pickle pickle序列化模块 【一】定义 序列化:将数据结构或对象转换为可存储或传输的格式反序列化:将序列化后的数据恢复为开始的数据结构或者对象 【二】目的 数据持久化存储远程通信缓存进程间通信 【三】序列化 将对象转换为字节…...
CentOS 6 制作openssh 9.6 p1 rpm包(含ssh-copy-id、openssl) —— 筑梦之路
openssh 9.6 需要openssl 1.1.1 以上版本,因此需要先安装openssl 1.1.1,可阅读这篇升级更新openssl版本到1.1.1w CentOS 6 制作openssl 1.1.1w rpm包 —— 筑梦之路-CSDN博客 CentOS 6很久都停止更新和支持,关于此版本的写的不多ÿ…...
Tomcat Notes: Deployment File
This is a personal study notes of Apache Tomcat. Below are main reference material. - YouTube Apache Tomcat Full Tutorial,owed by Alpha Brains Courses. https://www.youtube.com/watch?vrElJIPRw5iM&t801s 1、Tomcat deployment1.1、Two modes of …...
某邦通信股份有限公司IP网络对讲广播系统挖矿检测脚本
目录 1.漏洞概述 2.影响版本 3.危害等级 4.挖矿程序检测 5.Nuclei自动化检测...
uniapp点击跳转传对象
目录 传对象传对象传送组件接受组件 最后 传对象 传对象 传送组件 点击传给组件 <view class"dki-tit-edit" click"gotificatedit(item)">编辑 </view>gotificatedit(item){console.log(item,item);let options JSON.stringify(item);uni.…...
简单用PHP实现微信小程序的游戏功能
微信小程序的兴起,越来越多的开发者开始关注如何在小程序中实现游戏功能。PHP作为一种流行的后端语言,可以很好地与小程序进行搭配,实现游戏功能。下面将介绍如何使用PHP来实现微信小程序的游戏功能,并提供具体的代码示例。 建立…...
某查查请求头参数加密分析(含JS加密算法与Python爬虫源码)
文章目录 1. 写在前面2. 请求分析3. 断点分析4. 扣加密JS5. Python爬虫代码实现 【作者主页】:吴秋霖 【作者介绍】:Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作! 【作者推荐】ÿ…...
免费用chatGPT
免费用chatGPT,地址: DocGPT - 第二大脑...
还不会python 实现常用的数据编码和对称加密?看这篇文章就够啦~
相信很多使用 python 的小伙伴在工作中都遇到过,对数据进行相关编码或加密的需求,今天这篇文章主要给大家介绍对于一些常用的数据编码和数据加密的方式,如何使用 python 去实现。话不多说,接下来直接进入主题: 前言 1…...
简易实现 MyBatis 底层机制
MyBatis 大家好呀!我是小笙,我中间有1年没有更新文章了,主要忙于毕业和就业相关事情,接下来,我会恢复更新!我们一起努力吧! 概述 MyBatis 是一个持久层的框架(前身是 ibatis&#x…...
PhpPythonC++圆类的实现(OOP)
哎......被投诉了 😭😭😭😭😭 其实也不是小编不更,这不是期末了吗(zhaojiekou~~),而且最近学的信息收集和ctf感觉好像没找到啥能更的(不过最经还是在考虑更一…...
OpenSSL升级版本
1 查看openssl版本 $ openssl version OpenSSL 1.0.2k-fips 26 Jan 2017 目前是1.0版本系列. 2 下载最新稳定版本的OpenSSL源码包 $ wget https://www.openssl.org/source/openssl-1.1.1q.tar.gz 3 编译源码安装 tar -xzvf openssl-1.1.1q.tar.gz cd openssl-1.1.1q .…...
基于sprinmgboot实习管理系统源码和论文
随着信息化时代的到来,管理系统都趋向于智能化、系统化,实习管理也不例外,但目前国内仍都使用人工管理,市场规模越来越大,同时信息量也越来越庞大,人工管理显然已无法应对时代的变化,而实习管理…...
图像分类任务的可视化脚本,生成类别json字典文件
1. 前言 之前的图像分类任务可视化,都是在train脚本里, 用torch中dataloader将图片和类别加载,然后利用matplotlib库进行可视化。 如这篇文章中:CNN 卷积神经网络对染色血液细胞分类(blood-cells) 在分类任务中,必定…...
Adding Conditional Control to Text-to-Image Diffusion Models——【代码复现】
官方实现代码地址:lllyasviel/ControlNet: Let us control diffusion models! (github.com) 一、前言 此项目的使用需要显存大于8G,训练自己的ControlNet或需要更大,因此请注意查看自身硬件是否符合。 在此之前请确保已经安装好python以及…...
java-Exchanger详解
1.概述 java.util.concurrent.Exchanger。这在Java中作为两个线程之间交换对象的公共点。 2.Exchanger简介 Exchanger类可用于在两个类型为T的线程之间共享对象。该类仅提供了一个重载的方法exchange(T t)。 当调用exchanger时,它会等待成对的另一个线程也调用它…...
东莞网站建设服务有什么用/seo搜索引擎优化总结报告
转载自: http://blog.csdn.net/u014610226/article/details/47679323 以下是对C中不能重载为友元函数的四个运算符进行了详细的分析介绍,需要的朋友可以过来参考下 C规定有四个运算符 , ->, [], ()不可以是全局域中的重载(即不能重载为友员函数)&…...
4399网站做游戏赚钱/中央人民政府
我听到的一些发声 你们赚的钱已经可以了: 我一个发小是做土木工程的,上海大学博士,参与很多著名建筑的工程,但是从薪资上看,还不如一些稍微像样的公司的6年多的高级开发。为什么?这就是行业的红利…...
wordpress主题手机主题/百度文库个人登录
为什么 redis 单线程却能支撑高并发? 纯内存操作 核心是基于非阻塞的 IO 多路复用机制 单线程反而避免了多线程的频繁上下文切换问题 一、Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快(纯内存); 数据存在内存中,数据结构用H…...
台湾做电商网站/网站推广计划方案
客户关系管理系统(CRM)是现代企业管理中不可或缺的管理工具之一。一个好的**CRM系统**可以帮助企业更好地管理客户信息、提高销售和市场营销效率、增强客户忠诚度等。但是,如何选择适合自己企业的CRM系统呢?以下是一些选择CRM系统…...
浙江响应式网站建设公司/分析网站
西门子获首都机场18亿大单 早报记者 周玲 责任编辑 罗裕 2005-5-24 0:10:24 记者从西门子中国公司获悉,西门子日前从北京首都国际机场获得了一份价值约为18亿元的新订单。西门子将为北京机场提供一套用于新型T3航站楼的先进的行李处理系统。预期此项工程将在2…...
不备案的网站能打开吗/好用搜索引擎排名
主要参考文档: 中文:http://wiki.centos.org/zh/HowTos/VNC-Server 英文:http://wiki.centos.org/HowTos/VNC-Server 描述很规范、全面。 记录下安装过程中知识点/要点: 1.linux是一个内核,需安装一个窗口管理员来取得…...