什么是 WebSocket? 

  随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据。
  我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
  Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这 样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过 TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小 了很多。本文不详细地介绍WebSocket规范,主要介绍下WebSocket在Java Web中的实现。
  JavaEE 7中出了JSR-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持 WebSocket,从7.0.47开始支持JSR-356,下面的Demo代码也是需要部署在Tomcat7.0.47以上的版本才能运行。
  WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
以下 API 用于创建 WebSocket 对象。
  var Socket = new WebSocket(url, [protocol] );
以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。

实现方式

  1. 常用的 Node 实现有以下三种。

  1. Tomcat实现websocket方法
  2. spring整合websocket方法

具体实现

  • Tomcat实现websocket方法

使用这种方式无需别的任何配置,只需服务端一个处理类

服务端

/**
* 服务器
* @ClassName: WebSocket
* @Description: TODO
* @author OnlyMate
* @Date 2018年8月16日 下午2:46:54
*
*/
@ServerEndpoint("/webSocketByTomcat/{username}")
public class WebSocket {
private static int onlineCount = 0;
private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
private Session session;
private String username; @OnOpen
public void onOpen(@PathParam("username") String username, Session session) throws IOException { this.username = username;
this.session = session; addOnlineCount();
clients.put(username, this);
System.out.println("已连接"); } @OnClose
public void onClose() throws IOException {
clients.remove(username);
subOnlineCount();
} @OnMessage
public void onMessage(String message) throws IOException {
JSONObject jsonTo = JSONObject.parseObject(message);
System.out.println(jsonTo.getString("to") +":"+ jsonTo.getString("msg")); if (!jsonTo.getString("to").toLowerCase().equals("all")){
sendMessageTo(jsonTo.getString("msg"), jsonTo.getString("to"));
}else{
sendMessageAll(jsonTo.getString("msg"));
}
} @OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
} public void sendMessageTo(String message, String To) throws IOException {
// session.getBasicRemote().sendText(message);
//session.getAsyncRemote().sendText(message);
for (WebSocket item : clients.values()) {
if (item.username.equals(To) )
item.session.getAsyncRemote().sendText(message);
}
} public void sendMessageAll(String message) throws IOException {
for (WebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
} public static synchronized int getOnlineCount() {
return onlineCount;
} public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
} public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
} public static synchronized Map<String, WebSocket> getClients() {
return clients;
}
}

客户端

<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<c:set var="ctxpath"
value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" />
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset=UTF-8">
<title>登录测试</title>
</head>
<body>
<h2>Hello World!</h2>
<div>
<span>sessionId:</span>
<%
HttpSession s= request.getSession();
out.println(s.getId());
%>
</div> <input id="sessionId" type="hidden" value="<%=session.getId() %>" />
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<hr />
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr />
<div id="message"></div>
</body>
<script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script>
<script type="text/javascript">
var websocket = null;
if('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);
} else if('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);
} else {
websocket = new SockJS("localhost:8088/websocket/webSocketByTomcat/"+document.getElementById('sessionId').value);
} //连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
}; //连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
} //接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
} //连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
} //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
} //将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
} //关闭WebSocket连接
function closeWebSocket() {
websocket.close();
} //发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>

注意导入socketjs时要使用地址全称,并且连接使用的是http而不是websocket的ws

服务端如何向客户端推送消息呢?

代码如下

/**
* 服务端推送消息对客户端
* @ClassName: ServiceClientController
* @Description: TODO
* @author OnlyMate
* @Date 2018年8月16日 下午2:45:22
*
*/
@Controller
@RequestMapping(value="webSocketByTomcat/serviceToClient")
public class ServiceClientByTomcatController {
private WebSocket websocket = new WebSocket(); @RequestMapping
public void sendMsg(HttpServletRequest request, HttpServletResponse response) throws IOException {
JSONObject json = new JSONObject();
json.put("to", request.getSession().getId());
json.put("msg", "欢迎连接WebSocket!!!!");
websocket.onMessage(json.toJSONString());
}
}

效果如下图所示

 

 
  • spring整合websocket方法

springboot对websocket支持很友好,只需要继承webSocketHandler类,重写几个方法就可以了

这个类是对消息的一些处理,比如是发给一个人,还是发给所有人,并且前端连接时触发的一些动作

/**
* 创建一个WebSocket server
*
* @ClassName: CustomWebSocketHandler
* @Description: TODO
* @author OnlyMate
* @Date 2018年8月16日 下午3:17:34
*
*/
@Service
public class CustomWebSocketHandler extends TextWebSocketHandler implements WebSocketHandler {
private Logger logger = LoggerFactory.getLogger(CustomWebSocketHandler.class);
// 在线用户列表
private static final Map<String, WebSocketSession> users;
// 用户标识
private static final String CLIENT_ID = "mchNo"; static {
users = new HashMap<>();
} @Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
logger.info("成功建立websocket-spring连接");
String mchNo = getMchNo(session);
if (StringUtils.isNotEmpty(mchNo)) {
users.put(mchNo, session);
session.sendMessage(new TextMessage("成功建立websocket-spring连接"));
logger.info("用户标识:{},Session:{}", mchNo, session.toString());
}
} @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
logger.info("收到客户端消息:{}", message.getPayload());
JSONObject msgJson = JSONObject.parseObject(message.getPayload());
String to = msgJson.getString("to");
String msg = msgJson.getString("msg");
WebSocketMessage<?> webSocketMessageServer = new TextMessage("server:" +message);
try {
session.sendMessage(webSocketMessageServer);
if("all".equals(to.toLowerCase())) {
sendMessageToAllUsers(new TextMessage(getMchNo(session) + ":" +msg));
}else {
sendMessageToUser(to, new TextMessage(getMchNo(session) + ":" +msg));
}
} catch (IOException e) {
logger.info("handleTextMessage method error:{}", e);
}
} @Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
logger.info("连接出错");
users.remove(getMchNo(session));
} @Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
logger.info("连接已关闭:" + status);
users.remove(getMchNo(session));
} @Override
public boolean supportsPartialMessages() {
return false;
} public void sendMessage(String jsonData) {
logger.info("收到客户端消息sendMessage:{}", jsonData);
JSONObject msgJson = JSONObject.parseObject(jsonData);
String mchNo = StringUtils.isEmpty(msgJson.getString(CLIENT_ID)) ? "陌生人" : msgJson.getString(CLIENT_ID);
String to = msgJson.getString("to");
String msg = msgJson.getString("msg");
if("all".equals(to.toLowerCase())) {
sendMessageToAllUsers(new TextMessage(mchNo + ":" +msg));
}else {
sendMessageToUser(to, new TextMessage(mchNo + ":" +msg));
}
} /**
* 发送信息给指定用户
* @Title: sendMessageToUser
* @Description: TODO
* @Date 2018年8月21日 上午11:01:08
* @author OnlyMate
* @param mchNo
* @param message
* @return
*/
public boolean sendMessageToUser(String mchNo, TextMessage message) {
if (users.get(mchNo) == null)
return false;
WebSocketSession session = users.get(mchNo);
logger.info("sendMessage:{} ,msg:{}", session, message.getPayload());
if (!session.isOpen()) {
logger.info("客户端:{},已断开连接,发送消息失败", mchNo);
return false;
}
try {
session.sendMessage(message);
} catch (IOException e) {
logger.info("sendMessageToUser method error:{}", e);
return false;
}
return true;
} /**
* 广播信息
* @Title: sendMessageToAllUsers
* @Description: TODO
* @Date 2018年8月21日 上午11:01:14
* @author OnlyMate
* @param message
* @return
*/
public boolean sendMessageToAllUsers(TextMessage message) {
boolean allSendSuccess = true;
Set<String> mchNos = users.keySet();
WebSocketSession session = null;
for (String mchNo : mchNos) {
try {
session = users.get(mchNo);
if (session.isOpen()) {
session.sendMessage(message);
}else {
logger.info("客户端:{},已断开连接,发送消息失败", mchNo);
}
} catch (IOException e) {
logger.info("sendMessageToAllUsers method error:{}", e);
allSendSuccess = false;
}
} return allSendSuccess;
} /**
* 获取用户标识
* @Title: getMchNo
* @Description: TODO
* @Date 2018年8月21日 上午11:01:01
* @author OnlyMate
* @param session
* @return
*/
private String getMchNo(WebSocketSession session) {
try {
String mchNo = session.getAttributes().get(CLIENT_ID).toString();
return mchNo;
} catch (Exception e) {
return null;
}
}
}

这个类的作用就是在连接成功前和成功后增加一些额外的功能

我们希望能够把websocketSession和httpsession对应起来,这样就能根据当前不同的session,定向对websocketSession进行数据返回;在查询资料之后,发现spring中有一个拦截器接口,HandshakeInterceptor,可以实现这个接口,来拦截握手过程,向其中添加属性

/**
* WebSocket握手时的拦截器
* @ClassName: CustomWebSocketInterceptor
* @Description: TODO
* @author OnlyMate
* @Date 2018年8月16日 下午3:17:04
*
*/
public class CustomWebSocketInterceptor implements HandshakeInterceptor {
private Logger logger = LoggerFactory.getLogger(CustomWebSocketInterceptor.class);
/**
* 关联HeepSession和WebSocketSession,
* beforeHandShake方法中的Map参数 就是对应websocketSession里的属性
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
logger.info("*****beforeHandshake******");
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest();
HttpSession session = httpServletRequest.getSession(true); logger.info("mchNo:{}", httpServletRequest.getParameter("mchNo"));
if (session != null) { map.put("sessionId",session.getId());
map.put("mchNo", httpServletRequest.getParameter("mchNo"));
}
}
return true;
} @Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
logger.info("******afterHandshake******");
}
}

这个类是配置类向Spring中注入handler

/**
* websocket的配置类
* @ClassName: CustomWebSocketConfig
* @Description: TODO
* @author OnlyMate
* @Date 2018年8月16日 下午3:17:26
*
*/
@Configuration
@EnableWebSocket
public class CustomWebSocketConfig implements WebSocketConfigurer { @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(customWebSocketHandler(), "/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*");
registry.addHandler(customWebSocketHandler(), "/sockjs/webSocketBySpring/customWebSocketHandler").addInterceptors(new CustomWebSocketInterceptor()).setAllowedOrigins("*").withSockJS();
} @Bean
public WebSocketHandler customWebSocketHandler() {
return new CustomWebSocketHandler();
}
}

补充说明:

setAllowedOrigins("*")一定要加上,不然只有访问localhost,其他的不予许访问

setAllowedOrigins(String[] domains),允许指定的域名或IP(含端口号)建立长连接,如果只允许自家域名访问,这里轻松设置。如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头

经查阅官方文档springwebsocket 4.1.5版本前默认支持跨域访问,之后的版本默认不支持跨域,需要设置

使用withSockJS()的原因:

  一些浏览器中缺少对WebSocket的支持,因此,回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。

SockJS的一大好处在于提供了浏览器兼容性。优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。 
除此之外,spring也对socketJS提供了支持。

如果代码中添加了withSockJS()如下,服务器也会自动降级为轮询。

registry.addEndpoint("/coordination").withSockJS();

SockJS的目标是让应用程序使用WebSocket API,但在运行时需要在必要时返回到非WebSocket替代,即无需更改应用程序代码。

SockJS是为在浏览器中使用而设计的。它使用各种各样的技术支持广泛的浏览器版本。对于SockJS传输类型和浏览器的完整列表,可以看到SockJS客户端页面。 
传输分为3类:WebSocket、HTTP流和HTTP长轮询(按优秀选择的顺序分为3类)

客户端

<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<c:set var="ctxpath"
value="${pageContext.request.scheme}${'://'}${pageContext.request.serverName}${':'}${pageContext.request.serverPort}${pageContext.request.contextPath}" />
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset=UTF-8">
<title>登录测试</title>
</head>
<body>
<h2>Hello World! Web Socket by Spring</h2>
<div>
<span>sessionId:</span>
<%
HttpSession s= request.getSession();
out.println(s.getId());
%>
</div> <input id="sessionId" type="hidden" value="<%=session.getId() %>" />
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<hr />
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr />
<div id="message"></div>
</body>
<script type="text/javascript" src="http://localhost:8088/static/js/sockjs-0.3.min.js"></script>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);
} else if('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://localhost:8088/websocket/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);
} else {
websocket = new SockJS("http://localhost:8088/websocket/sockjs/webSocketBySpring/customWebSocketHandler?mchNo="+ 123);
}
//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
}; //连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
} //接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
} //连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
} //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
} //将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
} //关闭WebSocket连接
function closeWebSocket() {
websocket.close();
} //发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>

效果如图所示

完毕

WebSocket实践——Java实现WebSocket的两种方式的更多相关文章

  1. 对Java代码加密的两种方式,防止反编译

    使用Virbox Protector对Java项目加密有两种方式,一种是对War包加密,一种是对Jar包加密.Virbox Protector支持这两种文件格式加密,可以加密用于解析class文件的j ...

  2. Java新建线程的两种方式

    Java新建线程有两种方式,一种是通过继承Thread类,一种是实现Runnable接口,下面是新建线程的两种方式. 我们假设有个竞赛,有一个选手A做俯卧撑,一个选手B做仰卧起坐.分别为两个线程: p ...

  3. Java实现多线程的两种方式

    实现多线程的两种方式: 方式1: 继承Thread类 A: 自定义MyThread类继承Thread类 B: 在MyThread类中重写run() C: 创建MyThread类的对象 D: 启动线程对 ...

  4. [Java] HashMap遍历的两种方式

    Java中HashMap遍历的两种方式原文地址: http://www.javaweb.cc/language/java/032291.shtml第一种: Map map = new HashMap( ...

  5. Java实现深克隆的两种方式

    序列化和依次克隆各个可变的引用类型都可以实现深克隆,但是序列化的效率并不理想 下面是两种实现深克隆的实例,并且测试类对两种方法进行了对比: 1.重写clone方法使用父类中的clone()方法实现深克 ...

  6. java文件读写的两种方式

    今天搞了下java文件的读写,自己也总结了一下,但是不全,只有两种方式,先直接看代码: public static void main(String[] args) throws IOExceptio ...

  7. K:java中序列化的两种方式—Serializable或Externalizable

    在java中,对一个对象进行序列化操作,其有如下两种方式: 第一种: 通过实现java.io.Serializable接口,该接口是一个标志接口,其没有任何抽象方法需要进行重写,实现了Serializ ...

  8. java实现同步的两种方式

    同步是多线程中的重要概念.同步的使用可以保证在多线程运行的环境中,程序不会产生设计之外的错误结果.同步的实现方式有两种,同步方法和同步块,这两种方式都要用到synchronized关键字. 给一个方法 ...

  9. Java线程创建的两种方式

    java多线程总结一:线程的两种创建方式及优劣比较 (一)---之创建线程的两种方式 java实现多线程的两种方法的比较

  10. java创建线程的两种方式及源码解析

    创建线程的方式有很多种,下面我们就最基本的两种方式进行说明.主要先介绍使用方式,再从源码角度进行解析. 继承Thread类的方式 实现Runnable接口的方式 这两种方式是最基本的创建线程的方式,其 ...

随机推荐

  1. Base64编码的 换行 转义

    用Base64编码的时候如果出现\n 之类的字符,java中转义. 当字符串过长(一般超过76)时会自动在中间加一个换行符,字符串最后也会加一个换行符.导致和其他模块对接时结果不一致. 解决方法:将  ...

  2. Uva12230Crossing Rivers (数学期望)

    问题: You live in a village but work in another village. You decided to follow the straight path betwe ...

  3. CF 148D D. Bag of mice (概率DP||数学期望)

    The dragon and the princess are arguing about what to do on the New Year's Eve. The dragon suggests ...

  4. bzoj 4852 炸弹攻击

    bzoj 4852 炸弹攻击 二维平面上的最优解问题,模拟退火是一个较为优秀的近似算法. 此题确定圆心后,便可 \(O(m)\) 算出收益,且最优解附近显然也较优,是连续变化的,可以直接模拟退火. 小 ...

  5. BZOJ2152 聪聪可可 【点分治】

    BZOJ2152 聪聪可可 Description 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)--遇到这种问 ...

  6. CALayer2-创建新的层

    本文目录 一.添加一个简单的图层 二.添加一个显示图片的图层 三.为什么CALayer中使用CGColorRef和CGImageRef这2种数据类型,而不用UIColor和UIImage? 四.UIV ...

  7. Java 并发:volatile 关键字解析

    摘要: 在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保 ...

  8. python---time 相关, str 转timestamp

    df['col'] = pd.to_datetime(df['col']) from datetime import date from datetime import datetime d = da ...

  9. 使用 lego生成 Let's Encrypt 证书

    1. 工具 https://github.com/xenolf/lego   2. 使用 命令生成新的 lego --email="foo@bar.com" --domains=& ...

  10. php基础语法(数据类型、运算符)

    数据类型 标量类型: int, float, string, bool 复合类型: array, object 特殊类型: null, resouce 整数类型int, integer 字符串类型st ...