使用websocket创建一个简单的即时通信工具
程序员文章站
2022-05-21 13:43:54
...
一、关于websocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。--百度百科
简单来说,就是服务器和客户端可以实现双向推送消息。http协议是只能由客户端发起请求,服务器作出响应。这样的效率是很低的。
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
二、前台代码实现
css
body{
background-color: #efebdc;
}
#hz-main{
width: 750px;
height: 500px;
/*background-color: red;*/
margin: 0 auto;
border-radius:5px;
margin-top: 9%;
}
#hz-message{
width: 500px;
height: 500px;
float: right;
background-color: #ffffff;
border-radius:0px 10px 10px 0;
}
#hz-message-body{
width: 460px;
height: 320px;
background-color: #f9f9f9;
/*background-color: #E0C4DA;*/
padding: 10px 20px;
overflow:auto;
border-radius:0px 10px 0px 0;
}
#hz-message-input{
width: 500px;
height: 99px;
background-color: white;
overflow:auto;
outline: none;
}
#hz-group{
width: 200px;
height: 500px;
background-color: #c7d1ea;
/*background-color: rosybrown;*/
float: right;
border-radius:0px 0px 0px 0px;
}
#hz-set{
width: 50px;
height: 500px;
float: left;
background-color: #2a2a2b;
border-radius:10px 0px 0px 10px;
color: #ebebeb;
}
.hz-message-list{
min-height: 30px;
margin: 10px 0;
}
.hz-message-list-text{
padding: 7px 13px;
border-radius: 15px;
width: auto;
max-width: 85%;
display: inline-block;
}
.hz-message-list-time{
padding: 7px 13px;
text-align: center;
font-size: 12px;
}
.hz-message-list-username{
margin: 0;
}
.hz-group-body{
overflow:auto;
}
.hz-group-list{
padding: 10px;
border-bottom:1px solid #ffffff;
}
.left{
float: left;
color: #595a5a;
background-color: #ebebeb;
}
.right{
float: right;
color: #f7f8f8;
background-color: #919292;
}
.hz-badge{
width: 20px;
height: 20px;
background-color: #FF5722;
border-radius: 50%;
float: right;
color: white;
text-align: center;
line-height: 20px;
font-weight: bold;
opacity: 0;
}
.img-head{
margin: 5px;
width: 35px;
height: 35px;
border-radius:50%
}
.img-logout{
margin: 370px 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.img-big{
width: 20px;
height: 20px;
cursor: pointer;
float: right;
}
html
<!--<!DOCTYPE>-->
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>聊天页面</title>
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<link th:href="@{/css/socketChart.css}" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.js"></script>
</head>
<body style="background-image: url('/static/img/webchat.png');background-size: 100%">
<div id="hz-main">
<div id="hz-message">
<!-- 头部 -->
<span id="toUserName"></span>
<span id="toUserId" hidden></span>
<!-- <hr style="margin: 0px;"/>-->
<!-- 主体 -->
<div id="hz-message-body">
</div>
<!-- 功能条 -->
<div id="">
<button>表情</button>
<button id="upload-img">图片</button>
<button>文件</button>
<button id="jietu">截图</button>
<button onclick="send()" style="float: right;cursor: pointer;">发送(Ctrl+Enter)</button>
</div>
<input type="file" id="btn_file" style="display:none">
<!-- 输入框 -->
<div contenteditable="true" id="hz-message-input">
</div>
</div>
<div id="hz-group">
<div id="hz-group-body"> </div>
</div>
<div id="hz-set">
<img class="img-head" src="/img/login.png" alt="头像"/>
<span id="talks" th:text="${username}" style="text-align: center;display:block;">请登录</span><span id="talksid" th:text="${userid}" hidden></span>
<br/>
<span id="onlineCount" hidden>0</span>
<img class="img-logout" src="/static/img/tuichuimg.png" title="退出" id="img-logout" onclick="logout()"/> <!--onclick="logout()"-->
<!-- <button style="color: #ffffff;background-color: #2a2a2b;margin: 350px 5px;border:none;cursor: default;" onclick="tuichu()">退出</button>-->
</div>
</div>
</body>
<script type="text/javascript" th:inline="javascript">
//项目路径
ctx = [[${#httpServletRequest.getContextPath()}]];
//登录名
var username = /*[[${username}]]*/'';
var userid = /*[[${userid}]]*/'';
</script>
<script th:src="@{/js/socketChart.js}"></script>
</html>
js
//消息对象数组
var msgObjArr = new Array();
var websocket = null;
//判断当前浏览器是否支持WebSocket, springboot是项目名
if ('WebSocket' in window) {
websocket = new WebSocket("ws://"+window.location.hostname+":10092/websocket/"+userid+"//"+username);
} else {
console.error("不支持WebSocket");
}
//连接发生错误的回调方法
websocket.onerror = function (e) {
console.error("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function () {
//获取所有在线用户
$.ajax({
type: 'post',
url: ctx + "/websocket/getOnlineList",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {username:username},
success: function (data) {
var onlineCount = 0;
if (data.length) {
//列表
for (var i = 0; i < data.length; i++) {
var user = eval(data[i]);
var userName = user.userName;
var userId = user.id;
var userString1 = user.string1;
if(userString1=="在线"){
onlineCount++;
}
$("#hz-group-body").append("<div class=\"hz-group-list\"><span hidden class='hz-group-list-userid'>" + userId + "</span><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" >["+userString1+"]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
//在线人数
$("#onlineCount").text(onlineCount);
// $("#onlineCount").text(data.length);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
var messageJson = eval("(" + event.data + ")");
//普通消息(私聊)
if (messageJson.type == "1") {
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message;
//增加聊天数据
setMessageInnerHTML(srcUser.username,srcUser.username, message);
}
//普通消息(群聊)
if (messageJson.type == "2"){
//来源用户
var srcUser = messageJson.srcUser;
//目标用户
var tarUser = messageJson.tarUser;
//消息
var message = messageJson.message;
//增加聊天数据
setMessageInnerHTML(username,tarUser.username, message);
}
//对方不在线
if (messageJson.type == "0"){
//消息
var message = messageJson.message;
$("#hz-message-body").append(
"<div class=\"hz-message-list\" style='text-align: center;'>" +
"<div class=\"hz-message-list-text\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
}
//在线人数
if (messageJson.type == "onlineCount") {
// getOnLineList();
//取出username
var onlineCount = messageJson.onlineCount;
var userName = messageJson.username;
var userId = messageJson.userid;
var oldOnlineCount = $("#onlineCount").text();
//新旧在线人数对比
if (oldOnlineCount < onlineCount) {
if($("#" + userName + "-status").length > 0){
$("#" + userName + "-status").text("[在线]");
}else{
$("#hz-group-body").append("<div class=\"hz-group-list\"><span hidden class='hz-group-list-userid'>" + userId + "</span><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\">[在线]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
} else {
//有人下线
$("#" + userName + "-status").text("[离线]");
}
$("#onlineCount").text(onlineCount);
}
}
//连接关闭的回调方法
websocket.onclose = function () {
//alert("WebSocket连接关闭");
}
function getOnLineList(){
$("#hz-group-body").empty();
$.ajax({
type: 'post',
url: ctx + "/websocket/getOnlineList",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {username:username},
success: function (data) {
var onlineCount = 0;
if (data.length) {
//列表
for (var i = 0; i < data.length; i++) {
var user = eval(data[i]);
var userName = user.userName;
var userId = user.id;
var userString1 = user.string1;
if(userString1=="在线"){
onlineCount++;
}
$("#hz-group-body").append("<div class=\"hz-group-list\"><span hidden class='hz-group-list-userid'>" + userId + "</span><span class='hz-group-list-username'>" + userName + "</span><span id=\"" + userName + "-status\" >["+userString1+"]</span><div id=\"hz-badge-" + userName + "\" class='hz-badge'>0</div></div>");
}
//在线人数
$("#onlineCount").text(onlineCount);
// $("#onlineCount").text(data.length);
}
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}
//将消息显示在对应聊天窗口 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
function setMessageInnerHTML(srcUserName,msgUserName, message) {
console.log("setMessageInnerHTML");
//判断
var childrens = $("#hz-group-body").children(".hz-group-list");
var isExist = false;
for (var i = 0; i < childrens.length; i++) {
var text = $(childrens[i]).find(".hz-group-list-username").text();
if (text == srcUserName) {
isExist = true;
break;
}
}
if (!isExist) {
$("#hz-group-body").append("<div class=\"hz-group-list\"><span class='hz-group-list-username'>" + srcUserName + "</span><span hidden class='hz-group-list-userid'>" + srcUserName + "</span><span id=\"" + srcUserName + "-status\">[在线]</span><div id=\"hz-badge-" + srcUserName + "\" class='hz-badge'>0</div></div>");
} else {
}
// 对于接收消息来说这里的toUserName就是来源用户,对于发送来说则相反
var username = $("#toUserName").text();
//刚好打开的是对应的聊天页面
if (srcUserName == username) {
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<p class='hz-message-list-username'>"+msgUserName+":</p>" +
"<div class=\"hz-message-list-text left\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
} else {
//小圆点++
var conut = $("#hz-badge-" + srcUserName).text();
$("#hz-badge-" + srcUserName).text(parseInt(conut) + 1);
$("#hz-badge-" + srcUserName).css("opacity", "1");
}
endScroll();
}
//发送消息
function send() {
//消息
var message = $("#hz-message-input").html().replace("<div><br></div>/g,",'');
if(null==message||message==""){
alert("发送消息不能为空")
return;
}
//目标用户名
var tarUserName = $("#toUserName").text();
var tarUserId = $("#toUserId").text();
if(null==tarUserName||tarUserName==""){
alert("请选择用户")
return;
}
//登录用户名
var srcUserName = $("#talks").text();
var srcUserId = $("#talksid").text();
websocket.send(JSON.stringify({
"type": "1",
"tarUser": {"username": tarUserName,"userid": tarUserId},
"srcUser": {"username": srcUserName,"userid": srcUserId},
"message": message
}));
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<div class=\"hz-message-list-text right\">" +
"<span>" + message + "</span>" +
"</div>" +
"</div>");
$("#hz-message-input").html("");
endScroll();
}
//监听点击用户
$("body").on("click", ".hz-group-list", function () {
$(".hz-group-list").css("background-color", "");
$(this).css("background-color", "whitesmoke");
$("#toUserName").text($(this).find(".hz-group-list-username").text());
$("#toUserId").text($(this).find(".hz-group-list-userid").text());
//清空旧数据,从对象中取出并追加
$("#hz-message-body").empty();
$("#hz-badge-" + $("#toUserName").text()).text("0");
$("#hz-badge-" + $("#toUserName").text()).css("opacity", "0");
var toUserId = $("#toUserId").text();
var toUserName = $("#toUserName").text();
//查询聊天记录
$.ajax({
type: 'get',
url: ctx + "/websocket/getMessageList",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {fromId:userid,toId:toUserId},
success: function (data) {
if (data.length) {
console.log("messageList============="+data)
//遍历data,放入msgObjArr中
var messageArr = new Array();
for (let i = 0; i < data.length; i++) {
var message = eval(data[i]);
messageArr.push({
toUserName: toUserName,
message: [{username: message.fromName, message: message.message, date: message.sendTime}]
});
}
for (let i = 0; i < messageArr.length; i++) {
var messageChannelList = messageArr[i].message;
if(messageChannelList.length>0){
for (var j = 0; j < messageChannelList.length; j++) {
var msgObj = messageChannelList[j];
var leftOrRight = "right";
var message = msgObj.message;
var msgUserName = msgObj.username;
var date = msgObj.date;
var toUserName = $("#toUserName").text();
if (msgUserName == toUserName) {
leftOrRight = "left";
}
if (username == toUserName && msgUserName != toUserName) {
leftOrRight = "left";
}
if (username == toUserName && msgUserName == toUserName) {
leftOrRight = "right";
}
var magUserName = leftOrRight == "left" ? "<p class='hz-message-list-username'>"+msgUserName+":</p>" : "";
$("#hz-message-body").append(
"<div class=\"hz-message-list\">" +
"<div class=\"hz-message-list-time\">" +
"<span>" + date + "</span>" +
"</div>" +
magUserName+
"<div class=\"hz-message-list-text " + leftOrRight + "\">" +
"<span>" + message + "</span>" +
"</div>" +
"<div style=\" clear: both; \"></div>" +
"</div>");
}
}
}
}
endScroll();
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
});
document.onkeydown=function(event){
if(event.ctrlKey && event.keyCode == 13){
send();
}
};
/**
* 聊天显示区域自动到底
*/
function endScroll() {
var area = $("#hz-message-body")[0];
area.scrollTop = area.scrollHeight;
}
/**
* 退出登录
*/
function logout() {
$.ajax({
type: 'post',
url: ctx + "/logout",
contentType: 'application/json;charset=utf-8',
dataType: 'json',
data: {username:username},
success: function (data) {
// console.log()
window.location.href=ctx+"/login";
},
error: function (xhr, status, error) {
console.log("ajax错误!");
}
});
}
$("#upload-img").on("click",function () {
document.getElementById("btn_file").click();
})
$("#btn_file").on("change",function () {
var $file = $(this);
var fileObj = $file[0];
var windowURL = window.URL || window.webkitURL;
var dataURL;
var imgObj = document.createElement("img");
if (fileObj && fileObj.files && fileObj.files[0]) {
dataURL = windowURL.createObjectURL(fileObj.files[0]);
imgObj.src= dataURL;
} else {
dataURL = $file.val();
imgObj.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale)";
imgObj.filters.item("DXImageTransform.Microsoft.AlphaImageLoader").src = dataURL;
}
$("#hz-message-input").html(imgObj);
});
$("#jietu").on("click",function () {
html2canvas($(".img-head"), {
// html2canvas($("#hz-message-body"), {
allowTaint: true,
taintTest: false,
onrendered: function(canvas) {
canvas.id = "mycanvas";
//document.body.appendChild(canvas);
//生成base64图片数据
var dataUrl = canvas.toDataURL();
var newImg = document.createElement("img");
newImg.src = dataUrl;
newImg.className = 'img';
$("#hz-message-input").html(newImg);
// $(".product").before(newImg);
}
});
});
$("#img-big").on("click",function () {
});
三、后台代码实现
/**
* WebSocket服务
*/
@RestController
@RequestMapping("/websocket")
@ServerEndpoint(value = "/websocket/{userid}/{username}", configurator = MyEndpointConfigure.class)
public class WebSocketServer {
/**
* 在线人数
*/
private static int onlineCount = 0;
/**
* 在线用户的Map集合,key:用户名,value:Session对象
*/
private static Map<User, Session> sessionMap = new ConcurrentHashMap<>();
/**
* 注入其他类(换成自己想注入的对象)
*/
// @Resource
// private MessageService messageService;
@Resource
private UserService userService;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userid") int userid, @PathParam("username") String username) {
//在webSocketMap新增上线用户
User user = new User();
user.setId(userid);
user.setUserName(username);
sessionMap.put(user, session);
//在线人数加加
WebSocketServer.onlineCount++;
//通知除了自己之外的所有人
sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username+"'}");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
//下线用户名
String logoutUserName = "";
//从webSocketMap删除下线用户
for (Entry<User, Session> entry : sessionMap.entrySet()) {
if (entry.getValue() == session) {
sessionMap.remove(entry.getKey());
logoutUserName = entry.getKey().getUserName();
break;
}
}
//在线人数减减
WebSocketServer.onlineCount--;
//通知除了自己之外的所有人
sendOnlineCount(session, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + logoutUserName + "'}");
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
//JSON字符串转 HashMap
HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class);
//消息类型
String type = (String) hashMap.get("type");
//来源用户
Map srcUser = (Map) hashMap.get("srcUser");
//目标用户
Map tarUser = (Map) hashMap.get("tarUser");
//如果点击的是自己,那就是群聊
if (srcUser.get("username").equals(tarUser.get("username"))) {
//群聊
groupChat(session,hashMap);
} else {
//私聊
privateChat(session, tarUser, hashMap);
}
//后期要做消息持久化
Message mess = new Message();
mess.setFromId(Integer.parseInt((String) srcUser.get("userid")));
mess.setFromName((String) srcUser.get("username"));
mess.setToId(Integer.parseInt((String) tarUser.get("userid")));
mess.setToName((String) tarUser.get("username"));
mess.setMessage((String) hashMap.get("message"));
// messageService.insertMessage(mess);
userService.insertMessage(mess);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 通知除了自己之外的所有人
*/
private void sendOnlineCount(Session session, String message) {
for (Entry<User, Session> entry : sessionMap.entrySet()) {
try {
if (entry.getValue() != session) {
entry.getValue().getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 私聊
*/
private void privateChat(Session session, Map tarUser, HashMap hashMap) throws IOException {
//获取目标用户的session
boolean boo = false;
Session tarUserSession = null;
for (User key : sessionMap.keySet()) {
if(key.getUserName().equals((String)tarUser.get("username"))){
boo=true;
tarUserSession=sessionMap.get(key);
}
}
//如果不在线则发送“对方不在线”回来源用户
if (!boo) {
session.getBasicRemote().sendText("{\"type\":\"0\",\"message\":\"对方不在线\"}");
} else {
hashMap.put("type", "1");
tarUserSession.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
}
/**
* 群聊
*/
private void groupChat(Session session,HashMap hashMap) throws IOException {
for (Entry<User, Session> entry : sessionMap.entrySet()) {
//自己就不用再发送消息了
if (entry.getValue() != session) {
hashMap.put("type", "2");
entry.getValue().getBasicRemote().sendText(new ObjectMapper().writeValueAsString(hashMap));
}
}
}
/**
* 登录
*/
@RequestMapping("/login/{username}")
public ModelAndView login(HttpServletRequest request, @PathVariable String username) {
//插入用户表
User user = new User();
user.setUserName(username);
user.setPassWord("123456");
int userid = userService.insertUser(user);
ModelAndView mav = new ModelAndView("socketChart");
mav.addObject("username",username);
mav.addObject("userid",userid);
return mav;
}
/**
* 登出
*/
@RequestMapping("/logout/{username}")
public String loginOut(HttpServletRequest request, @PathVariable String username) {
return "退出成功!";
}
/**
* 获取在线用户
*/
@RequestMapping("/getOnlineList")
private List<User> getOnlineList(String username) {
List<User> allUserList = userService.searchAllUser();
for (int i = 0; i < allUserList.size(); i++) {
User user = allUserList.get(i);
//判断用户是否在线
boolean boo = false;
for (Entry<User, Session> entry : WebSocketServer.sessionMap.entrySet()) {
if (entry.getKey().getUserName().equals(user.getUserName())) {
user.setString1("在线");
boo=true;
break;
}
}
if(!boo){
user.setString1("离线");
}
}
return allUserList;
}
/**
* 查询聊天记录
*/
@RequestMapping("/getMessageList")
private List<Message> getMessageList(int fromId, int toId) {
List<Message> list = userService.searchMessage(fromId, toId);
return list;
}
}
package com.lianxu.websocket;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.websocket.server.ServerEndpointConfig;
/**
* 解决注入其他类的问题,详情参考这篇帖子:webSocket无法注入其他类:https://blog.csdn.net/tornadojava/article/details/78781474
*/
public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
private static volatile BeanFactory context;
@Override
public <T> T getEndpointInstance(Class<T> clazz){
return context.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MyEndpointConfigure.context = applicationContext;
}
}
package com.lianxu.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置
*/
@Configuration
public class WebSocketConfig {
/**
* 用途:扫描并注册所有携带@ServerEndpoint注解的实例。 @ServerEndpoint("/websocket")
* PS:如果使用外部容器 则无需提供ServerEndpointExporter。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
/**
* 支持注入其他类
*/
@Bean
public MyEndpointConfigure newMyEndpointConfigure (){
return new MyEndpointConfigure ();
}
}
四、最终效果实现
点击自己是群聊 点击其他人是私聊
五、问题总结
1.截图,发送文件
2.聊天界面放大缩小
3.添加好友分组
4.设置头像
参考:https://www.cnblogs.com/huanzi-qch/p/10821246.html
推荐阅读
-
使用WebSocket实现即时通讯(一个群聊的聊天室)
-
使用visualStudio2017创建一个简单的控制台程序
-
使用最基础的Node,创建一个简单的node.js应用
-
使用WebSocket进行通信的简单应用
-
使用websocket创建一个简单的即时通信工具
-
PHP 麻烦哪位教小弟我使用Zend Studio创建一个简单的PHP工程,并调试运行
-
使用visualStudio2017创建一个简单的控制台程序
-
使用IDEA创建一个简单的hibernate项目
-
使用maven创建一个简单的hibernate项目(IDEA)
-
使用idea创建一个简单的Spring Boot(Maven)项目-图文详解