Java使用WebSocket实现即时通信,微信简易小游戏交互机制,经验总结

小编最近在使用WebSocket实现小游戏交互,虽然简单的游戏很简单,但要实现游戏中双方的交互效果能够实时展现,达到完美的情况花费了不少时间精力,中间会遇到客户端问题,断网问题,以及客户端链接问题,在此,小编将这些日子所积累的经验发布在此博客,欢迎大家留言讨论。

Java 部分:

引入依赖:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>5.3.29</version>
        </dependency>

创建WebSocket程序启动文件 WebSocketConfig

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Component
public class WebSocketConfig{
	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		System.out.println("启动websocket支持");
		return new ServerEndpointExporter();
	}
}

2.创建WebSocket核心服务文件:CommonWebSocketService

import com.alibaba.fastjson.JSONObject;
import com.haoyue.rabbitmq.MsgProducer;
import com.haoyue.util.data.DataService;
import com.haoyue.util.string.StringEncryptUtil;
import com.haoyue.util.string.StringTimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
@EnableScheduling
@ServerEndpoint("/api/webSocket")
public class CommonWebSocketService {
	private Session session;

	@Autowired
	private DataService dataService;

	@Autowired
	public MsgProducer msgProducer;
	
	/**
	* 使用线程安全的Map存储 用于存储WebSocket链接id与Session的映射关系
	* LinkedList 是双向链表 有序数据集合 List是无序数据集合
	* ConcurrentHashMap 是线程安全的MAp 确保多线程使用的时候  这个变量不会出问题
	*/
	public static final ConcurrentHashMap<String, CommonWebSocketService> sessions_data = new ConcurrentHashMap<>();
	public static final LinkedList<ConcurrentHashMap<String, Object>> sessions_connect = new LinkedList<>();
	
	/**
	*此定时器用于每间隔30秒给所有客户端发送心跳续连包,因为WebSocket长时间未交互会发生自动断开链接
	*/
	@Scheduled(initialDelay = 5000, fixedRate = 30000)
	public void websocketz_heartbeat() throws InterruptedException {
		try {
			System.gc();
			JSONObject heartbeatpack=new JSONObject();
			heartbeatpack.put("type","heartbeat");
			heartbeatpack.put("content","");
			heartbeatpack.put("code","200");
			send_to_Message_all(heartbeatpack.toJSONString());
		}catch (Exception e){
			String error_title=e.getMessage();
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			String exceptionStackTrace = sw.toString();

			log.info("\r\n心跳续连包 发送异常:"+error_title+"----------\n"+exceptionStackTrace);

		}

	}
	
	/**
	* 此定时器用于判断前端发来的心跳,长时间未收到心跳,则移除此客户端链接
	*/
	@Scheduled(initialDelay = 5000, fixedRate = 40000)
	public void applets_heartbeat(){
		try {
			for (int i = 0; i < sessions_connect.size(); i++) {
				if(sessions_connect.get(i)==null){sessions_connect.remove(i);continue;}
				int isstate=Integer.parseInt(sessions_connect.get(i).get("isstate")+"");
				//移除离线客户端
				if(isstate>1){
					String linkeid=sessions_connect.get(i).get("linkeid")+"";
					sessions_data.remove(linkeid); sessions_connect.remove(i);
					System.out.println(linkeid+":websocket已离线");
				}
			}
			for (int i = 0; i < sessions_connect.size(); i++){
				int isstate=Integer.parseInt(sessions_connect.get(i).get("isstate")+"");
				isstate=isstate+1; sessions_connect.get(i).put("isstate",isstate);
			}
		}
		catch (Exception e){
			String error_title=e.getMessage();
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			String exceptionStackTrace = sw.toString();

			log.info("\r\nwebsocket链接在线判断处理异常:"+error_title+"----------\n"+exceptionStackTrace);
		}

	}
	
	//建立前端与后端的链接
	@OnOpen
	public void onOpen(Session session,@PathParam("param")String param) throws InterruptedException {
		try {
			this.session = session;
			String canshu = session.getQueryString();
			if (!StringEncryptUtil.Is_Null(canshu) && canshu.split("=").length == 2) {
				String linkid = canshu.split("=")[1];//客户端id,小编这里采用以微信用户的id作为客户端链接的唯一标识,实现每个小程序用户都会有独立的WebSocket链接

				//原有的客户端链接已失效 移除原有的客户端链接
				for (int i = 0; i < sessions_connect.size(); i++) {
					if(sessions_connect.get(i)==null){sessions_connect.remove(i);continue;}
					if(sessions_connect.get(i).get("linkid").equals(linkid)){sessions_connect.remove(i);}
				}
				sessions_data.put(linkid,this);
				ConcurrentHashMap<String, Object> connect = new ConcurrentHashMap<>();
				connect.put("linkid",linkid); connect.put("isstate",0); sessions_connect.add(connect);

				Thread.sleep(4000);//延迟四秒
				//发送消息给小程序 链接成功
				JSONObject senddata = new JSONObject();
				senddata.put("websocket_id",linkid);
				senddata.put("type","java_websocketsuccess");
				senddata.put("content","链接成功");
				senddata.put("code","200");
				send_to_Message(senddata.toJSONString());

			}
			log.info("\r\n【websocket消息】有新的连接, 独立链接总数:{}", sessions_connect.size());
		}catch (Exception e){
			System.out.println(e);
		}
	}
	
	//关闭前端与后端的链接
	@OnClose
	public void onClose(Session session,@PathParam("param")String param) {
		try{
			String canshu=session.getQueryString();
			if(!StringEncryptUtil.Is_Null(canshu)&&canshu.split("=").length==2){
				String linkid=canshu.split("=")[1]; sessions_data.remove(linkid);
				for (int i = 0; i < sessions_connect.size(); i++) {
					if(sessions_connect.get(i)==null){sessions_connect.remove(i);continue;}
					if(sessions_connect.get(i).get("linkid").equals(linkid)){
						sessions_connect.remove(i);
					}
				}
			}
			log.info("\r\n【websocket消息】连接断开, 独立链接总数:{}", sessions_connect.size());
		}catch (Exception e){
			String error_title=e.getMessage();
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			String exceptionStackTrace = sw.toString();

			log.info("\r\nwebsocket链接关闭异常 :"+error_title+"----------\n"+exceptionStackTrace);
		}
	}
	
	//接收前端向WebSocket服务发来的消息 由于后端WebSocket带有注解的方法均属于静态方法,小编也在此进行小程序的游戏数据交互和前端的心跳接收
	@OnMessage
	public void onMessage(String message) throws InterruptedException {
		try{

			JSONObject dataall=JSONObject.parseObject(message);
			log.info("【websocket消息】收到客户端发来的消息:"+dataall.get("type"));
			if(dataall.get("type").equals("java_topgameingdata")){
				//发送小程序游戏用户操作数据给游戏的对方
				dataall.put("type","applets_gameinprogress");
				dataall.put("websocket_id",dataall.get("rival_id"));
				dataall.remove("rival_id");
				send_to_Message(dataall.toJSONString());
			}
			else if(dataall.get("type").equals("applets_heartbeat")){
				//接收小程序的心跳,用于更新WebSocket客户端的在线状态
				String linkid=dataall.get("linkid")+"";
				for (int i = 0; i < sessions_connect.size(); i++) {
					if(sessions_connect.get(i).get("linkid").equals(linkid)){ sessions_connect.get(i).put("isstate",0);}
				}
			}
		}catch (Exception e){
			System.out.println("消息异常:"+e);
		}
	}
	
	/**
	* 配置错误信息处理
	* @param session
	* @param t
	*/
	@OnError
	public void onError(Session session, Throwable t) {
		log.info("\r\n【websocket出现错误】:"+t.getMessage()+"------------------------"+t);
		// 打印异常的堆栈跟踪
		t.printStackTrace();
	}
	
	//发送websocket消息
	public Map<String,Object> send_to_Message(String message) throws InterruptedException {
		JSONObject data=JSONObject.parseObject(message);

		String id="";
		//这里对WebSocket发送的消息做了持久化处理,当用户发生断线重连时,可以写一个接口,将重要的未发送的消息进行重新发送,但几个消息除外
		//applets_gameinprogress 游戏运行数据类型 由于游戏运行中的交互属于静态交互,第一,不好使用@Autowired进行调方法,第二,为了优化游戏的运行性能,所以不用进行持久化
		//java_websocketsuccess和heartbeat均属于不重要的消息,所以不必要进行持久化
		if(!data.get("type").equals("applets_gameinprogress")&&!data.get("type").equals("java_websocketsuccess")&&!data.get("type").equals("heartbeat")){
			if(StringEncryptUtil.Is_Null(data.get("base_websocket_id")+"")){

				//新增websocket消息放入数据库 持久化
				Map<String,Object> insert=new HashMap<>();
				insert.put("websocket_id",data.get("websocket_id")); insert.put("type",data.get("type"));
				insert.put("content",message); insert.put("issend",0);
				insert.put("time_date", StringTimeUtil.now());
				id=dataService.dataInsertUtil.Insert("base_websocket",insert);

				data.put("base_websocket_id",id);
			}
			else{
				id=data.get("base_websocket_id")+"";
				data.put("base_websocket_id",id);
			}
		}

		boolean issended=false;
		try{
			JSONObject sendstr=new JSONObject(data);
			if(StringEncryptUtil.Is_Null(data.get("websocket_id")+"")){return null;}
			CommonWebSocketService webSocket=sessions_data.get(data.get("websocket_id"));
			if(webSocket!=null){
				try{ send(webSocket,sendstr.toJSONString());issended=true; }
				catch (Exception r){
					//当消息发送异常时候 移除客户端链接
					sessions_data.remove(data.get("websocket_id"));
					for (int j = 0; j < sessions_connect.size(); j++) {
						if(sessions_connect.get(j).get("linkid").equals(data.get("websocket_id"))){sessions_connect.remove(j);}
					}
					String error_title=r.getMessage(); StringWriter sw = new StringWriter();
					PrintWriter pw = new PrintWriter(sw); r.printStackTrace(pw); String exceptionStackTrace = sw.toString();
					log.info("\r\nwebsocket独立消息发送异常:"+error_title+"----------\n"+exceptionStackTrace);
				}
			}
			if(!issended){ log.info("\r\n【websocket】独立消息类型:"+data.get("type")+"----"+data.get("websocket_id")+":未连接");}
		}
		catch (Exception e){
			e.printStackTrace();
			log.info("\r\nwebsocket异常信息是:"+e);
		}
		if(issended){ log.info("\r\n【websocket独立消息】"+data.get("type")+"发送完成----"+data.get("websocket_id"));}
		Map<String,Object> result=new HashMap<>();
		result.put("issend",issended);
		result.put("base_websocket_id",id);
		return result;
	}

	//此方法用于给所有客户端发送全体消息,比如游戏大厅的游戏房间刷新
	public void send_to_Message_all(String message) throws InterruptedException {
		JSONObject data=JSONObject.parseObject(message); data.put("websocket_id","all"); boolean issended=false;
		try{
			System.out.println("发送全部链接消息 独立链接总数"+sessions_connect.size());
			String id="";
			if(StringEncryptUtil.Is_Null(data.get("base_websocket_id")+"")&&!data.get("type").equals("heartbeat")){
				Map<String,Object> insert=new HashMap<>();
				insert.put("websocket_id",data.get("websocket_id")); insert.put("type",data.get("type"));
				insert.put("content",message); insert.put("issend",0);
				insert.put("time_date", StringTimeUtil.now());
				id=dataService.dataInsertUtil.Insert("base_websocket",insert);
				data.put("base_websocket_id",id);
			}
			for (Map.Entry<String, CommonWebSocketService> e:sessions_data.entrySet()) {
				if(e.getValue()==null){ sessions_data.remove(e.getKey()); continue; }
				CommonWebSocketService webSocket=e.getValue();
				System.out.println(e.getKey()+" websocket链接发送数据类型:"+data.get("type"));

				try { send(webSocket,message);issended=true; }
				catch (Exception r){
					sessions_data.remove(e.getKey());
					for (int j = 0; j < sessions_connect.size(); j++) {
						if(sessions_connect.get(j).get("linkid").equals(e.getKey())){sessions_connect.remove(j);}
					}
					String error_title=r.getMessage(); StringWriter sw = new StringWriter();
					PrintWriter pw = new PrintWriter(sw); r.printStackTrace(pw); String exceptionStackTrace = sw.toString();
					log.info("\r\nwebsocket独立消息全部 发送异常:"+error_title+"----------\n"+exceptionStackTrace);
				}
				if(!data.get("type").equals("heartbeat")){
					//发送异步综合RabbitMQ消息 将持久化的websocket消息改为已发送
					Map<String,Object> senddata=new HashMap<>();
					senddata.put("type","java_websocketissend");
					senddata.put("data",id);
					JSONObject rabbitmqdata = new JSONObject(senddata);
					msgProducer.send_zongheDirectExchange("zonghe_shop_activity", rabbitmqdata.toJSONString());
				}
			}
			if(!issended){ log.info("\r\n【websocket】全部独立消息类型:"+data.get("type")+"----"+data.get("websocket_id")+":未连接");}
		}
		catch (Exception e){
			e.printStackTrace();
			log.info("\r\nwebsocket发送全部消息,发送数据为:"+data.get("type")+"----websocket链接:"+data.get("websocket_id"));
			log.info("\r\nwebsocket发送全部消息异常是:"+e);
		}
		if(issended){
			log.info("\r\n【websocket发送全部独立消息】"+data.get("type")+"发送完成----"+(StringEncryptUtil.Is_Null(data.get("websocket_id")+"")==true?"":data.get("websocket_id")));
		}
	}

	//这里注意,必须要加synchronized锁,因为真实环境中极有可能出现高并发的情况,如果不加锁会经常出现异常
	public synchronized void send(CommonWebSocketService webSocket,String message){
		JSONObject data=JSONObject.parseObject(message);
		try {
			webSocket.session.getAsyncRemote().sendText(message);
		}
		catch (Exception e){
			String error_title=e.getMessage(); StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); String exceptionStackTrace = sw.toString();
			log.info("\r\nwebsocket消息发送异常:"+error_title+"----------\n"+exceptionStackTrace);
		}
	}
}

以上就是Java的全部部分,下面是微信小程序部分,小编是采用的uniapp开发的小程序。
    首先,需要用户在进入小程序的时候就立即建立WebSocket链接,所以在APP.vue中的onLunch写入

小程序部分:

App.vue:

onLaunch: async function() {
	await this.opwebsocket();//建立链接
	await this.setthat();//启动前端向后端发送心跳
	
	console.log('App Launch');
},
globalData:{
	sockets:null,//保存客户端链接
	networkvalue:5,
},
methods:{
	//链接websocket并打开onMessage
	async opwebsocket(){
		//小编做了当用户登录成功以后,会执行一次此方法,读取缓存中的用户id,建立独立的客户端链接
		let base_user_id=uni.getStorageSync("base_user_id");
		if(base_user_id==undefined||base_user_id==""||base_user_id==null||base_user_id=="undefined"||base_user_id=="null"){base_user_id="";}
		
		//http请求使用ws,https请求使用wss
		let websocketurl="ws://本机ip:端口号/api/webSocket?linkId="+base_user_id; 
		
		this.globalData.sockets = await uni.connectSocket({
		url: websocketurl,
			success: (rs) => { console.log('WebSocket 连接'); },
		});
		
		await message.websocketopen();//打开客户端收消息
	},
	async starttimer(){ await this.message.timer(); },
	
	//websocket链接成功,收到连接成功的消息,请求接口发送所有未发送的消息
	async connectSuccess(){
		let that=this;console.log("链接成功");
	},
	
	//websocket收到消息,请求接口此消息已读
	async connectrecive(id){
		let that=this;
	},
}

小程序的第二个websocket核心处理文件,message.js   

export var isheart=0;//用于记录后端服务向前端发的心跳
export var isthat=false;//用于判断that是否已经指向了小程序页面
export var appletsheartbeat=null;//小程序端向后端服务发送心跳定时器
export var websocketlistener=null;//监听后端服务发送心跳定时器
export var networkresult=null;//记录网络连接变化的结果
export var listener=null;//listener用于记录网络变化的函数
export var that;//用于指向各个页面的this

//网络变化监听
export function networklisten(){
	if(networkresult==null){
		listener = function (res) {
			networkresult=res;
			if(!networkresult.isConnected){getApp().globalData.networkvalue=0;uni.showToast({ title: "网络断开", icon: "error" }); }
			if(networkresult.networkType=="none"||networkresult.networkType=="unknown"){getApp().globalData.networkvalue=0;}
			if(networkresult.isConnected||(networkresult.networkType=="4g"||networkresult.networkType=="5g")){ 
				if(getApp().globalData.networkvalue==0){
					stopnetworklisten(listener);
					that.resetwebsocket();
					setTimeout(function(){uni.showToast({ title: "网络已连接", icon: "success" });},3000);
				} 
			}
		}
		startnetworklisten(listener);
	}
}
//开启网络监听
export function startnetworklisten(listene){
	uni.onNetworkStatusChange(listene); 
}
//移除上一次的网络监听
export function stopnetworklisten(listene){
	uni.offNetworkStatusChange(listene);
	setTimeout(function(){startnetworklisten(listene);},3000);
}
export function websocketopen() {
	let socket=getApp().globalData.sockets;
	socket.onMessage(event => {
		let dataall=JSON.parse(event.data);let base_user_id=uni.getStorageSync("base_user_id");
		console.log("websocket消息类型:"+dataall.type);
		
		if(dataall.type=="heartbeat"){isheart=0;}
		if(dataall.type=="java_websocketsuccess"){ getApp().connectSuccess(); }//告诉后端连接成功
		//告诉后端消息已读,同意过滤掉心跳、连接成功消息和全体消息
		if(dataall.type!="heartbeat"&&dataall.type!="java_websocketsuccess"&&dataall.base_websocket_id!="all"){ getApp().connectrecive(dataall.base_websocket_id); }
		//用户当前所在页面
		let currentPagePath = getCurrentPages().pop().$page.fullPath;
		//后续的各种消息类型就按照需求做即可,这里就不做过多展示了
	});
	//socket关闭事件
	socket.onClose(event => {
	
	socket.close();clearInterval(appletsheartbeat);
		if(!getApp().globalData.sockets==null){
			getApp().globalData.sockets.close();clearInterval(appletsheartbeat);
			getApp().globalData.sockets=null;
		}
		setTimeout(function(){console.log("websocket断线重连中:"+JSON.stringify(event));websocketconnectreset();},500);
	});
}

//向websocket服务端发送消息
export function websocketsend(data) {
	let socket=getApp().globalData.sockets;
	if(socket!=null){ socket.send({ data: data});  }
}
//重新链接websocket
export async function websocketconnectreset(){
	getApp().opwebsocket(that);isheart=0;setTimeout(function(){ that.websocket(); },500);
}

//websocket30秒心跳监听
export async function websocketlisten() {
	if(websocketlistener==null){
		websocketlistener=setInterval(function(){
			isheart=isheart+1;
			console.log("心跳值:"+isheart);
			if(isheart>2){
				console.log("websocket未收到心跳,重连中");clearInterval(appletsheartbeat);
				if(getApp().globalData.sockets!=null){
					getApp().globalData.sockets.close();clearInterval(appletsheartbeat);
					if(getApp().globalData.sockets!=null){getApp().globalData.sockets=null;}
					setTimeout(function(){websocketconnectreset();isheart=0;},500);
				}
			}
		},30000);
	}
}

//赋值指向this
export function websocketthat(_that) { that=_that;isthat=true;}

export function timer(){
	let data={};data.type="applets_heartbeat"; data.linkid=uni.getStorageSync("base_user_id");
	appletsheartbeat=setInterval(function(){websocketsend(JSON.stringify(data));},40000);//每隔40秒向后台发送心跳
}

以上就是小程序链接后端WebSocket服务的核心两个文件

小编还在小程序的各个页面做了一些方法,每个页面都需要做的

async onLoad() {
		await this.websocket();
}
methods:{
	//网络波动后的重新链接
	resetwebsocket(){
		let that=this;
		if(getApp().globalData.networkvalue==0){ getApp().globalData.networkvalue=5;
			setTimeout(function(){ console.log("网络波动后启动重连!"); that.message.websocketconnectreset(); },1000);
		}
	},
	//赋值指向this
	async websocket(){ await this.message.websocketthat(this); },
}

经验总结:
    小编websocket经常出现的两种异常:第一种:

websocket独立消息发送异常:The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for called method
	java.lang.IllegalStateException: The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for called method
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.checkState(WsRemoteEndpointImplBase.java:1274)
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.textStart(WsRemoteEndpointImplBase.java:1236)
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.sendStringByCompletion(WsRemoteEndpointImplBase.java:213)
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.sendStringByFuture(WsRemoteEndpointImplBase.java:201)
	at org.apache.tomcat.websocket.WsRemoteEndpointAsync.sendText(WsRemoteEndpointAsync.java:53)
	at com.haoyue.common.service.CommonWebSocketService.send(CommonWebSocketService.java:326)
	at com.haoyue.common.service.CommonWebSocketService.send_to_Message(CommonWebSocketService.java:236)
	at com.haoyue.special.screen.service.BigScreenService.bascdata(BigScreenService.java:153)
	at com.haoyue.special.screen.controller.BigScreenController.ranking(BigScreenController.java:29)
	at sun.reflect.GeneratedMethodAccessor99.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

此异常在网络上有很多处理方法,小编这里就不做过多解释了。

null------------------------java.io.EOFException

具体详细的异常暂时找不到了,此异常按照小编个人的经验理解来看,主要就是客户端链接的异常,客户端在做网络波动、断线重连,以及小程序关闭重新打开后的链接一定需要注意此异常,目前,对重连的地方做好精确的把控。

以上就是小编的全部分享,欢迎大家留言讨论

为什么需要websocket? 传统的实时交互的游戏,或服务器主动发送消息的行为(如推送服务),如果想做在微信上,可能你会使用轮询的方式进行,不过这太消耗资源,大量的请求也加重了服务器的负担,而且延迟问题比较严重。如果是自己开发的app,为了解决这些问题,很多团队会自建socket,使用tcp长链接、自定协议的方式与服务器进行相对实时的数据交互。有能力的团队,采用这种方式自然没什么大问题。不过小团队可能就要花费很多时间去调试,要解决很多难题,这个在成本上就划不来。 H5引入了webSocket来解决网页端的长链接问题,而微信小程序也支持websocket。这是一个非常重要的特性,所以本系列的文章会专门拿出一篇来讨论websocketwebSocket本质上也是TCP连接,它提供全双工的数据传输。一方面可以避免轮询带来的连接频繁建立与断开的性能损耗,另一方面数据可以是比较实时的进行双向传输(因为是长链接),而且WebSocket允许跨域通信(这里有个潜在的跨域安全的问题,得靠服务端来解决)。目前除IE外的浏览器已经对webSocket支持得很好了,微信小程序再推一把之后,它会变得更加流行。 我们来设计一个新的demo,一个比较有趣的小游戏,多人版扫雷,准确地讲,多人版挖黄金。 游戏规则是这样的:把雷换成金子,挖到金子加一分,每人轮流一次(A挖完轮到B,B挖完A才能再点击),点中金子就算你的,也不会炸,游戏继续,直到把场上所有的金子都挖完游戏才结束。跟扫雷一样,数字也是表示周边有几个金子,然后用户根据场上已经翻出来的数字来猜哪一格可能有金子。 这种交互的游戏难点在于,用户的点击操作都要传到服务器上,而且服务器要实时的推送到其它玩家的应用上。另外用户自己也要接收对方操作时实时传过来的数据,这样才不至于重复点中同一个格子。简单讲,就是你要上报操作给服务器,而服务器也要实时给你推消息。为了简化整个模型,我们规定玩家必须轮流来点击,玩家A点完后,才能轮到玩家B,玩家B操作完,玩家A才能点。 我们分几步来实现这个功能。 一、实现思路 1、第一步,我们要先生成扫雷的地图场景 这个算法比较简单,简述一下。随机取某行某列就可以定位一个格子,标记成金子(-1表示金子)。mimeCnt表示要生成的金子的数量,用同样的方式循环标记mimeCnt个随机格子。生成完后,再用一个循环去扫描这些-1的格子,把它周边的格子都加1,当然必须是非金子的格子才加1。代码放在这里。 其中increaseArround用来把这格金子周边的格子都加1,实现也比较简单: 执行genMimeArr(),随机生成结果如下: -1表示金子。看了下貌似没什么问题。接下去,我们就要接入webSocket了。 (这个是js版本的,其实生成地图场景的工作是在后台生成,这个js版本只是一个演示,不过算法是一样的。) 2、我们需要一个支持webSocket的服务端 本例子中,我们使用python的tornado框架来实现(tornado提供了tornado.websocket模块)。当然读者也可以使用socket.io,专为webSocket设计的js语言的服务端,用起来非常简单,它也对不支持webSocket的浏览器提供了兼容(flash或comet实现)。 笔者本人比较喜欢使用tornado,做了几年后台开发,使用最多的框架之一的就是它,NIO模型,而且非常轻量级,同样的rps,java可能需要700-800M的内存,tornado只要30-40M,所以在一台4G内存的机子上可以跑上百个tornado服务,而java,对不起,只能跑3个虚拟机。微服务的时代,这一点对小公司很重要。当然如果读者本人对java比较熟悉的话,也可以选择netty框架尝试一下。 webSocket用tornado的另一个好处是,它可以在同一个服务(端口)上同时支持webSocket及http两种协议。tornado的官方demo代码中展示了怎么实现同时使用两种协议。在本游戏中,可以这么用:用户进入首页,用http协议去拉取当前的房间号及数据。因为首页是打开最多的,进了首页的用户不一定会玩游戏。所以首页还没必要建立webSocket链接,webSocket链接主要用来解决频繁请求及推送的操作。首页只有一个请求操作。选了房间号后,进去下一个游戏页面再开始建立webSocket链接。 3、客户端 使用微信小程序开发工具,直接连接是会报域名安全错误的,因为工具内部做了限制,对安全域名才会允许连接。所以同样的,这里我们也继续改下工具的源码,把相关的行改掉就行修改方式如下: 找到asdebug.js的这一行,把它改成: if(false)即可。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吾疾唯君医

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值