精华内容
下载资源
问答
  • SSH框架如何与APP通信

    千次阅读 2016-11-05 14:46:48
    像我这种做APP出身的,就特想实现跟服务器通信。 要解决的问题就是如何接受APP传来的数据(一般为json格式)和返回数据(一般为json格式)。下面就进行一下说明。 如何接受APP传来的数据: 我是让action实现了...

    最近SSH肤浅的学了一遍后,也算自己向全栈迈出了重要的一步。现在学SSH框架大多以网站开发为例。像我这种做APP出身的,就特想实现跟服务器的通信。
    要解决的问题就是如何接受APP传来的数据(一般为json格式)和返回数据(一般为json格式)。下面就进行一下说明。
    如何接受APP传来的数据:
    我是让action实现了ServletRequestAware这个接口,然后重写了其方法获取request;然后从request得到输入流最后转为string,这样就能获取app传过来的数据,然后自己根据约定解析该json就好了。

    下面是action代码

    package com.yasin.action;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.struts2.interceptor.ServletRequestAware;
    import org.apache.struts2.interceptor.ServletResponseAware;
    
    import com.alibaba.fastjson.JSON;
    import com.opensymphony.xwork2.ActionSupport;
    import com.yasin.model.User;
    import com.yasin.service.IUserService;
    import com.yasin.utils.Utils;
    
    public class UserAction extends ActionSupport implements ServletRequestAware{
        private String status;//状态返回标记,用来告诉app该网络请求是否成功
    
        private HttpServletRequest request;//获取request
    
        /*
         * 上面变量相应的set,get方法
         * */
        public HttpServletRequest getRequest() {
            return request;
        }
    
        public void setRequest(HttpServletRequest request) {
            this.request = request;
        }
    
        public String getStatus() {
            return status;
        }
    
        public void setStatus(String state) {
            this.status = state;
        }
    
        private IUserService userService;
    
        public IUserService getUserService() {
            return userService;
        }
    
        public void setUserService(IUserService userService) {
            this.userService = userService;
        }
    
        /*
         * 用户注册功能
         * */
        public String addOrUpdateUser(){
            String content;
            try {
                content = Utils.stream2String(request.getInputStream());//将inputstream转为string,不知道怎么写,可以百度,很好找
                System.out.println(content);
                User user = JSON.parseObject(content,User.class);
                System.out.println(user.getUid()+","+user.getPwd());
                if(userService.addOrUpdateUser(user)){
                    status="0";//成功返回
                }else{
                    status="1";
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                status="0";
            }
    
            return SUCCESS;
        }
    
        /*
         * 登录功能
         * */
        public String login() throws IOException{
            String content = Utils.stream2String(request.getInputStream());
            System.out.println(content);
            User user = JSON.parseObject(content, User.class);
            if(userService.checkUser(user)!=null){
                status="0";
            }else{
                status="1";
            }
    
            return SUCCESS;
        }
    
        /*
         * ServletRequestAware接口必须要实现的方法,也是通过该方法获取请求的request
         */
        @Override
        public void setServletRequest(HttpServletRequest arg0) {
            // TODO Auto-generated method stub
            this.request = arg0;
        }
    
    }
    

    返回就在struts配置文件中就搞定了

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1//EN" "http://struts.apache.org/dtds/struts-2.1.dtd">
    <struts>
    <!-- 开启使用开发模式,详细错误提示 -->
        <!-- <constant name="struts.devMode" value="false" /> -->
        <!-- 将对象交给spring管理 -->
        <constant name="struts.objectFactory" value="spring" />
        <!-- 指定资源编码类型 -->
        <constant name="struts.i18n.encoding" value="UTF-8" /> 
        <!-- 指定每次请求到达,重新加载资源文件 -->
        <constant name="struts.i18n.reload" value="false" />
        <!-- 指定每次配置文件更改后,自动重新加载 -->
        <constant name="struts.configuration.xml.reload" value="false" />
        <!-- 默认后缀名 -->
        <!--     <constant name="struts.action.extension" value="do,action,jhtml,," /> -->
        <!-- Struts Annotation -->
        <!--     <constant name="actionPackages" value="com.test1"/> -->
    
        <package name="json" extends="json-default"  >
            <global-results>
                <result name="error">/Error.jsp</result>
            </global-results>
            <action name="register" class="userAction" method="addOrUpdateUser"> 
                <result name="success" type="json" >  
                         <param name="includeProperties">status.*</param> 
                </result>
            </action>
            <action name="login" class="userAction" method="login"> 
                <result name="success" type="json" >  
                         <param name="includeProperties">status.*</param> 
                </result>
            </action>
    
        </package>
    </struts>    
    

    不同于一般的配置文件,package我们使用json-defalut这个包,这些都是固定的,可以看到在每一个action中的result标签下的param的标签的name的值是固定的不用变,其值就是我们要返回的变量。
    像login action中,我们返回的变量填写了一个status(如果有多个变量,最后一个一定要以 .* 结尾),这样就把我们的变量已json的格式返回了。
    想login请求,如果成功返回的就是{“status”:”0”}。这里的变量要跟action的java代码中的变量要一样,否则找不到。

    —————————————–分割线———————————————-

    楼主自学后台开发刚入门,所以把自己的学习内容分享出来,写的可能不是业界常用的解决方案,但最起码是行的通的,也是给刚入门的同学降低点学习难度,不打击积极性。如有不当,求巨巨斧正。

    展开全文
  • 手机上的APP如何与服务器通信

    千次阅读 2019-12-07 16:24:39
    讲解CS通信之前,先大致了解一下我们平时手机通话的流程。语音信号经过脉冲采样变成数字信号,通过手机GSM模块发送无线信号至基站进入无线接入网,根据对方手机号查询数据库后通过骨干路由器转入核心网,一连串中.....

    文章转自本人公众号:机械猿,本人之前在四川某汽轮机从事结构强度设计,目前在阿里巴巴淘宝事业部担任高级开发工程师,有机械工程同行想转行IT,或者有想入职BAT的可以找我内推~

    絮叨

           讲解CS通信之前,先大致了解一下我们平时手机通话的流程。语音信号经过脉冲采样变成数字信号,通过手机GSM模块发送无线信号至基站进入无线接入网,根据对方手机号查询数据库后通过骨干路由器转入核心网,一连串中转之后发送到对端所属的小区,找一条空闲线路接通对方。

           网络通信类似,但是也有不同,电话信号只能维持一条连接,而一个服务端可以维持多条连接,像双十一淘宝OceanBase就达到了一千万QPS的并发量。

    这里实名给手淘打个招聘广告

    基础知识

    了解APP通信首先要了解socket的含义。Socket是一种进程通信方式,可用于多主机之间的通信,IP地址(对应主机)和端口(对应进程)就确定了一个socket,类似于电话的插座。下面我们来实现一个基础网络示例:客户端从标准输入读取文本,发送给服务器;服务器接收后原文返回给客户端,客户端输出到标准输出。

    注:标准输入STDIN位于 /dev/stdin ,一般为键盘输入,fd为0;标准输出STDOUT位于/dev/stdout,一般为终端显示器,fd为1;标准错误 STDERR位于/dev/stderr,fd为2。

           TCP客户/服务端程序基本流程如下:

    服务端处理流程

    服务端程序如下:

    #include<sys/socket.h>     /* basic socket definitions */
    int main(int argc, char **argv)
    {
           int listenfd,connfd;
           pid_t childpid;
           socklen_t clilen;
           struct sockaddr_incliaddr, servaddr;
    
           listenfd = Socket(AF_INET, SOCK_STREAM,0);  //创建套接字,监听端口
    
           bzero(&servaddr, sizeof(servaddr));
           servaddr.sin_family      = AF_INET;
           servaddr.sin_addr.s_addr =htonl(INADDR_ANY);
           servaddr.sin_port        = htons(SERV_PORT);
    
           Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));  //绑定本机地址
    
           Listen(listenfd, LISTENQ);      //监听
    
           for ( ; ; ) {
                  clilen = sizeof(cliaddr);
                  connfd = Accept(listenfd, (structsockaddr *) &cliaddr, &clilen);  //阻塞等待客户端SYN报文
    
                  if ( (childpid = Fork()) == 0) {      /* fork一个子进程专门处理接入的客户端 */
                         Close(listenfd);            /* 子进程关闭监听端口 */
                         str_echo(connfd);       /* 子进程发送请求 */
                         exit(0);
                  }
                  Close(connfd);                    /* 父进程关闭连接端口 */
           }
    }
    

    下面分析一下服务端状态机流程:

    服务端创建一个监听套接字并绑定本机知名端口(如80、8080http端口),本机地址设置为INADDR_ANY是为了任何本地接口的连接都接收,一般为多网卡的场景。之后服务端阻塞在accpt调用,使用fork为每个客户端专门分配一个子进程,父进程继续监听接入的客户端。

    对于已连接的客户端,使用str_echo读入客户端发送过来的数据,并直接返回回去。

    void str_echo(intsockfd)
    {
           ssize_t n;
           char buf[MAXLINE];
    
    again:
           while ( (n = read(sockfd, buf, MAXLINE))> 0)   //从标准输入读取数据
                  Writen(sockfd, buf, n);        //发送至服务端
                                              // while循环退出说明接收到FIN包,客户端完成了数据发送
           if (n < 0 && errno == EINTR)
                  goto again;                    //被信号打断,继续读取
           else if (n < 0)
                  err_sys("str_echo: readerror");   //遇到其他错误结束运行
    }

    客户端处理流程

    下面给出客户端处理状态机(省略部分socket异常处理):

    str_cli处理逻辑如下:

    void str_cli(FILE* fp, int sockfd)
    {
           charsendline[MAXLINE],recvline[MAXLINE];
    
           while (fgets(sendline, MAXLINE, fp) !=NULL) {  //从fp读入数据
    
                  Writen(sockfd, sendline,strlen(sendline));  //发送给服务器
    
                  if (Readline(sockfd, recvline,MAXLINE) == 0)   //接收服务器发送过来的数据
                         err_quit("str_cli:server terminated prematurely");  //如果为0,说明服务端已关闭连接
    
                  fputs(recvline, stdout);  //将接收到的数据输出到终端
           }   //文件读取结束时fgets返回NULL,while退出
    }
    

    运行客户端/服务端程序

    服务器启动后,在客户端连接之前,使用netstat -a检查主机监听套接字状态如下:

    Proto   Local Address       Foreign Address       State
    TCP     *:9877               *:*                LISTEN

    来启动客户端并指定服务器地址127.0.0.1(本地环回地址),客户端在connect函数中完成TCP三次握手流程,之后服务端从accept中返回,一条数据通道建立。

    服务端这边握手流程较为复杂,用简图表示如下:

    连接建立后,客户端阻塞于fgets等待接收键盘输入,服务端进程从accept返回后调用fork创建一个子进程专门负责这条连接,父进程继续阻塞在accept上监听新客户端的到来。此时,三个进程都阻塞:客户端进程、服务器父进程、服务器子进程。

    注1:一个程序不等于一个进程,像淘宝,除了主进程进行各种数据处理外,还有push进程作为维持客户端和服务器的长连接通信,用于发送心跳包和推送消息。

    注2:建立连接时,客户端阻塞在connect上,收到服务器的SYN/ACK报文即返回,而服务器需要收到ACK报文才返回,两边阻塞时间差了半个RTT。

    使用netstat -a观察现在连接情况:​​​​​​​

    Proto    Local Address           Foreign Address            StateTCP     localhost:9877           localhost:47512             ESTABLISHED //服务器TCP     localhost:47512          localhost:9877              ESTABLISHED    //客户端TCP     *:9877                   *:*                         LISTEN  //服务器父进程
          可以看到双方socket已处于ESTABLISHED状态,接下来客户端可以和服务器进行数据收发。当客户端输入EOF字符(按下Control+Z表示终止输入)时,fgets返回空指针,客户端数据处理函数str_cli返回,客户端main函数调用exit终止进程。进程终止会关闭所有打开的文件描述符,因此客户端会发送FIN报文给服务器,服务器子进程回应ACK后也调用exit函数关闭文件描述符,发送FIN报文。

    这里除了通过TCP四次挥手正常终止连接,还可以发送信号kill -9 pid终止进程。信号的处理后续剖析~

    上述程序对服务器主机崩溃、主机重启、主机关机及客户端主机崩溃等异常情况都做了保护,这也是我们平时写需要注意程序健壮性的地方。

    最后厚着脸皮推广一下自己的公众号:机械猿,有机械工程同行想转行IT,或者有想入职BAT的可以找我内推~

    展开全文
  • WebSocket作为一种解决web应用双向通信的协议由HTML5规范引出,是一种建立在TCP协议基础上的全双工通信...它是一个应用层协议,它出现是为了解决HTTP的痛点,希望在服务器与浏览器之间建立一条不受限制的双向通信的通道

    在这里插入图片描述

    系列

    OKHttp3–详细使用及源码分析系列之初步介绍【一】
    OKHttp3–流程分析 核心类介绍 同步异步请求源码分析【二】
    OKHttp3–Dispatcher分发器源码解析【三】
    OKHttp3–调用对象RealCall源码解析【四】
    OKHttp3–拦截器链RealInterceptorChain源码解析【五】
    OKHttp3–重试及重定向拦截器RetryAndFollowUpInterceptor源码解析【六】
    OKHttp3–桥接拦截器BridgeInterceptor源码解析及相关http请求头字段解析【七】
    OKHttp3–缓存拦截器CacheInterceptor源码解析【八】
    OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】
    通过ConnectInterceptor源码掌握OKHttp3网络连接原理 呕心沥血第十弹【十】
    OKHttp3-- 请求服务器拦截器CallServerInterceptor源码分析 【十一】

    前言

    这篇文章是有关OKHttp源码最后一篇文章了,OKHttp系列结束后准备对Glide出一个源码解读系列的博客;今天谈谈OKHttp对WebSocket的支持,WebSocket想必做Java开发的朋友们肯定很熟悉了,其实在Android上也是可以使用它的,至于怎么使用,就需要借助于OKHttp了

    本篇文章将从以下方面解读:什么是WebSocket,它的由来,它与HTTP、Socket的区别,如何在app上借助OKHttp进行WebSocket开发,OKHttp是如何实现WebSocket协议的。

    WebSocket

    WebSocket作为一种解决web应用双向通信的协议由HTML5规范引出,是一种建立在TCP协议基础上的全双工通信的协议。它是一个应用层协议,它出现是为了解决HTTP的痛点,希望在服务器与浏览器之间建立一条不受限制的双向通信的通道。

    为什么这么说呢?

    想想以前如果在浏览器里打开一个实时展示股票行情的页面,每时每刻的股票信息怎么从服务器发送到浏览器,是不是客户端定时发送http请求,然后服务端返回数据,比如以前很多网站都用Ajax做短轮询,浏览器发送一次请求,服务器返回结果,但是这有个问题是假如浏览器的某次请求,服务端并没有数据更新需要推送到浏览器,那这次请求就是一次浪费的网络操作;虽然后来出现了Comet技术,是一种基于Ajax的长轮询(long-polling)方式,服务器端接收到请求后,会阻塞请求直到有数据或者超时才返回,这种方式听起来就可以感觉到是一种互联网快速发展背景下的无奈之举,不管是谁,实质都是基于HTTP请求,而HTTP协议有很多比较难搞的问题,比如:

    • HTTP协议是一个请求–响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说HTTP请求必须是一个Request对应一个Response,在HTTP/1.0中,一个HTTP连接发送一个Request接收一个Response,连接就结束了;在HTTP/1.1中,出来一个keep-alive,一个HTTP连接可以发送多个Request,接收多个Response;但是一个Request=一个Response在HTTP中是亘古不变的准则,同时response也是被动的,不能主动发起
    • 还有每次HTTP请求都会经历三次握手-四次挥手的操作,频繁的执行该操作非常消耗服务器资源,虽然在HTTP/1.1推出了keep-alive机制,但是并没有改变请求–响应的实现机制,持久化连接只是减少了握手次数
    • 持久化连接带来了一个严重的问题,就是HOLB(Head of Line Blocking),即排头阻塞,因为同一个持久连接中的请求依然是串行的,当某一个请求因为网络、服务器等原因阻塞了,那后面的所有请求都得不到处理
    • 还有HTTP请求的头部太大,每次请求都要发送重复的数据,非常消耗流量
    • 它们的实时性没办法满足股票、聊天这种业务需求

    还有很多其它问题,这里就不再一一列举了,虽然这些问题在HTTP/2.0得到了很不错的解决,不过在它之前就有一种解决方式即WebSocket,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方

    与Socket、HTTP的关系

    虽然WebSocket名字里包含Socket,但是它跟Socket没有多少关系,Socket是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,不是协议;而WebSocket属于应用层协议。所以二者仅仅是名字像而已,类似于Java与JavaScript

    TCP是传输层协议,WebSocket和HTTP都是基于TCP的应用层协议,但是WebSocket是双向通信协议,HTTP 是单向的,同时WebSocket 是需要握手进行建立连接的;只是WebSocket 在建立握手时,数据是通过 HTTP 传输的,但是建立之后,在真正传输数据时候是不需要 HTTP 协议的,如下图
    在这里插入图片描述
    WebSocket只在连接建立时需要使用到HTTP协议,其余部分都是自己独立的

    WebSocket协议

    WebSocket连接的建立需要客户端发送特殊的HTTP请求进行握手,这里分享一个官方测试网站websocket.org,如下图

    在这里插入图片描述
    点击connect进行连接,这时候会发送一个HTTP请求进行连接,打开开发者工具可以看到其请求头部和响应头部信息如图
    在这里插入图片描述
    在这里插入图片描述

    可以看到这是一个类似于HTTP协议的报文,但是跟我们平时发送的HTTP请求头部信息是有很大不同的:

    • 该get请求的url地址不是类似http://这种,而是以ws://开头的
    • Connection: Upgrade表示连接类型是Upgrade,但是Upgrade是Upgrade: websocket,表示将该请求升级为websocket请求;这两个是Websocket的核心了,告诉Apache、Nginx等服务器,将这个连接转换为WebSocket连接
    • Sec-WebSocket-Key: gHTIArwwRC39dfextq+nfg==:是一个Base64 encode的值,这个是浏览器随机生成的,与响应头的该字段值进行比较,用于验证协议是否为WebSocket协议而非HTTP协议
    • Sec-WebSocket-Version:指定了WebSocket的协议版本

    同时可以看到响应头部的响应码【Status Code: 101 Web Socket Protocol Handshake】,响应码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议

    现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方,这后面就跟HTTP协议没啥关系了

    接下来再点击下发送按钮,看看开发者工具,如图
    在这里插入图片描述

    可以看到并没有产生新的连接,而是复用上面建立的WebSocket连接

    WebSocket作用

    说了这么多还没讲到WebSocket到底有什么作用,或者说有什么优点;上面讲到HTTP的痛点或者问题的时候提到过,当你的业务需要实时跟服务器交互,不管是短轮训还是长轮训,都需要消耗很大的服务器资源,还有服务器的被动性,还有每次都要发送重复的头部信息等,而WebSocket可以解决这些问题。

    WebSocket 解决的第一个问题是,通过第一个 HTTP request 建立了 TCP 连接之后,之后的交换数据都不需要再发 HTTP request了,使得这个长连接变成了一个真长连接。但是不需要发送 HTTP header就能交换数据显然和原有的 HTTP 协议是有区别的,所以它需要对服务器和客户端都进行升级才能实现。此外还有 multiplexing 功能,几个不同的 URI 可以复用同一个 WebSocket 连接。这些都是原来的 HTTP 不能做到的。

    WebSocket还是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求,就像使用Socket一样;而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。


    OKHttp与WebSocket

    WebSocket虽然是H5提出的,但不仅仅应用于Web应用上,在Android客户端,也可以使用,一般用下面两种库使用WebSocket:

    • OkHttp :16年OkHttp就加入了WebSocket支持包,最新版本已经将ws融合进来,直接可以使用
    • Java-WebSocket :Java实现的WebSocket协议

    这里以OKHttp为例,先看看如何基于OKHttp进行WebSocket开发

    public class WebSocketDemo {
    
        private final String TAG = WebSocketDemo.class.getSimpleName();
    
        private OkHttpClient CLIENT ;
        private WebSocket mWebSocket;
    
        private static final WebSocketDemo ourInstance = new WebSocketDemo();
    
        public static WebSocketDemo getDefault() {
            return ourInstance;
        }
    
        private WebSocketDemo() {
            CLIENT = new OkHttpClient.Builder()
                    .writeTimeout(5,TimeUnit.SECONDS)
                    .readTimeout(5, TimeUnit.SECONDS)
                    .connectTimeout(5, TimeUnit.SECONDS)
                    .build();
        }
    
        public void connect(String url){
            if (mWebSocket != null) {
                mWebSocket.cancel();
            }
            Request request = new Request.Builder()
                    .url(url)
                    .build();
            mWebSocket = CLIENT.newWebSocket(request,new SocketListener());
        }
    
        public void sendMessage(String message){
            mWebSocket.send(message);
        }
    
        public void sendMessage(byte... data){
            ByteString bs = ByteString.of(data);
            mWebSocket.send(bs);
        }
    
        public void close(int code, String reason){
            mWebSocket.close(code,reason);
        }
    
        class SocketListener extends WebSocketListener{
    
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                super.onOpen(webSocket, response);
                Log.i(TAG,"onOpen response="+response);
            }
    
            @Override
            public void onMessage(WebSocket webSocket, String text) {
                super.onMessage(webSocket, text);
                Log.i(TAG,"onMessage text="+text);
            }
    
            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                super.onMessage(webSocket, bytes);
                Log.i(TAG,"onMessage bytes="+bytes);
            }
    
            @Override
            public void onClosing(WebSocket webSocket, int code, String reason) {
                super.onClosing(webSocket, code, reason);
                Log.i(TAG,"onClosing code="+code);
            }
    
            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                super.onClosed(webSocket, code, reason);
                Log.i(TAG,"onClosed code="+code);
            }
    
            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                super.onFailure(webSocket, t, response);
                Log.i(TAG,"onFailure t="+t.getMessage());
            }
        }
    
    }
    

    可以看到用法非常简单,主要就是通过OkHttpClient实例化一个WebSocket,然后用来发送消息,同时还配置了一个回调,里面有各种回调方法,比如onOpen是在服务器连接成功后回调,onMessage是服务器发送消息给客户端时回调的,其它方法看名字也能看得懂,要注意的是这些方法都是在子线程回调的,不要直接更新UI。

    至于要实现什么定制需求,就看开发者自己在这个基础上进行封装了,比如你想实现IM即时通讯类的功能,那就在onMessage回调中要处理好消息数据的解析,判断数据属于哪个用户;实现消息推送功能简单点,数据解析没有那么复杂


    WebSocket源码

    接下来从源码看看OKHttp是如何实现WebSocket协议的

    第一步就是实例化一个WebSocket,代码如下

      @Override 
      public WebSocket newWebSocket(Request request, WebSocketListener listener) {
        RealWebSocket webSocket = new RealWebSocket(request, listener, new Random());
        webSocket.connect(this);
        return webSocket;
      }
        public RealWebSocket(Request request, WebSocketListener listener, Random random) {
          if (!"GET".equals(request.method())) {
            throw new IllegalArgumentException("Request must be GET: " + request.method());
          }
          this.originalRequest = request;
          this.listener = listener;
          this.random = random;
        
          byte[] nonce = new byte[16];
          random.nextBytes(nonce);
          this.key = ByteString.of(nonce).base64();
        
          this.writerRunnable = new Runnable() {
            @Override public void run() {
              try {
                while (writeOneFrame()) {
                }
              } catch (IOException e) {
                failWebSocket(e, null);
              }
            }
          };
        }
    

    RealWebSocket是WebSocket的实现类,其构造方法中第一个处理就是判断当前请求的方法是不是【GET】请求,如果不是,就抛出异常;原因在上面讲WebSocket协议的时候说过,它需要发送一个HTTP的get请求进行握手;接下来就是一些赋值,包括Sec-WebSocket-Key头的随机字符串,最后还实例化了一个writerRunnable,里面主要是一个while循环不断的从队列中取出数据发送到服务器

    接下来就是调用connect方法连接服务器

      public void connect(OkHttpClient client) {
        client = client.newBuilder()
            .protocols(ONLY_HTTP1)
            .build();
        final int pingIntervalMillis = client.pingIntervalMillis();
        final Request request = originalRequest.newBuilder()
            .header("Upgrade", "websocket")
            .header("Connection", "Upgrade")
            .header("Sec-WebSocket-Key", key)
            .header("Sec-WebSocket-Version", "13")
            .build();
        call = Internal.instance.newWebSocketCall(client, request);
        call.enqueue(new Callback() {
          @Override public void onResponse(Call call, Response response) {
            try {
              checkResponse(response);
            } catch (ProtocolException e) {
              failWebSocket(e, response);
              closeQuietly(response);
              return;
            }
    
            // Promote the HTTP streams into web socket streams.
            StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
            streamAllocation.noNewStreams(); // Prevent connection pooling!
            Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);
    
            // Process all web socket messages.
            try {
              listener.onOpen(RealWebSocket.this, response);
              String name = "OkHttp WebSocket " + request.url().redact();
              initReaderAndWriter(name, pingIntervalMillis, streams);
              streamAllocation.connection().socket().setSoTimeout(0);
              loopReader();
            } catch (Exception e) {
              failWebSocket(e, null);
            }
          }
    
          @Override public void onFailure(Call call, IOException e) {
            failWebSocket(e, null);
          }
        });
      }
    

    这里就能看的很明显了,分段来分析:

    • 在实例化Request的时候,添加了Upgrade、Connection等请求头,构建符合WebSocket协议握手规范的HTTP请求

    • 接下来获取到响应后调用checkResponse验证响应是否是符合规范的,比如响应码是否是101,有没有Upgrade、websocket响应头;Sec-WebSocket-Accept值是否与之间生成的随机字符串相同

        void checkResponse(Response response) throws ProtocolException {
          if (response.code() != 101) {
            throw new ProtocolException("Expected HTTP 101 response but was '"
                + response.code() + " " + response.message() + "'");
          }
      
          String headerConnection = response.header("Connection");
          if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
            throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
                + headerConnection + "'");
          }
      
          String headerUpgrade = response.header("Upgrade");
          if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
            throw new ProtocolException(
                "Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
          }
      
          String headerAccept = response.header("Sec-WebSocket-Accept");
          String acceptExpected = ByteString.encodeUtf8(key + WebSocketProtocol.ACCEPT_MAGIC)
              .sha1().base64();
          if (!acceptExpected.equals(headerAccept)) {
            throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
                + acceptExpected + "' but was '" + headerAccept + "'");
          }
        }
      
    • 检查合格后,WebSocket连接就正式建立了,这时候就将刚才的HTTP流转成web socket流,得到Streams这么个对象,定义如下

        public abstract static class Streams implements Closeable {
          public final boolean client;
          public final BufferedSource source;//输入流
          public final BufferedSink sink;//输出流
      
          public Streams(boolean client, BufferedSource source, BufferedSink sink) {
            this.client = client;
            this.source = source;
            this.sink = sink;
          }
        }
      
    • 接下来调用initReaderAndWriter实例化输出流输入流,得到WebSocketWriter/WebSocketReader,实际I/O操作由它们执行;同时设置Socket的超时时间为0,也就是永不超时,维持这个连接

    • 最后就是调用loopReader方法,循环读取服务器消息

        public void loopReader() throws IOException {
          while (receivedCloseCode == -1) {
            reader.processNextFrame();
          }
        }
        void processNextFrame() throws IOException {
            readHeader();
            if (isControlFrame) {
              readControlFrame();
            } else {
              readMessageFrame();
            }
        }
        private void readHeader() throws IOException {
            ......
              try {
                b0 = source.readByte() & 0xff;
              } finally {
                source.timeout().timeout(timeoutBefore, TimeUnit.NANOSECONDS);
              }
             ......
         }
      

      这里重点看下source.readByte方法,其Source实例化在Okio的source方法

        private static Source source(final InputStream in, final Timeout timeout) {
          return new Source() {
            @Override public long read(Buffer sink, long byteCount) throws IOException {
              try {
                ......
                int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
                return bytesRead;
              } catch (AssertionError e) {
                if (isAndroidGetsocknameError(e)) throw new IOException(e);
                throw e;
              }
            }
      		......
          };
        }
      

      可以看到最终调用的是Socket的输入流的read方法,这里也是IO阻塞模型,等待接收消息

    到此,WebSocket从连接建立到消息读取发送基本上分析完了,相信大家应该清楚了OKHttp是如何实现WebSocket协议的

    总结

    到这里,有关OKHttp源码解读系列也就结束了,从今年1月份开始,直到现在写了11篇它的源码解读文章,中间也停了三个多月,总的来看虽然花了这么多时间,但还是感触颇深,收获满满;站在巨人肩膀上能让你处在不同层次上看问题,也从中学到了不少代码设计模式,设计理念,看到一些功能的实现方式,也提高自己的代码能力,让我在自己的项目中也能借鉴一些开发经验,好处多多

    展开全文
  • 如何保证APP与服务端通信安全

    千次阅读 2020-04-18 19:17:24
    最严重的问题就是我们的APP与服务器通信接口没有加密处理被人抓包了,有人非法请求我们的接口获取数据。 怎么处理这个问题呢?领导又把这个光荣而艰巨的任务分给了我,没办法只能硬着头皮上啊,经过几天摸索终于...

    前言

    最近公司外包给别人做的一个APP项目上线了,拿到源码一看那代码质量真是一言难尽啊!

    刚上线用户比较少倒也没出啥问题,不过随着用户慢慢变多,问题逐渐暴露出来了。

    最严重的问题就是我们的APP与服务器的通信接口没有加密处理被人抓包了,有人非法请求我们的接口获取数据。

    怎么处理这个问题呢?领导又把这个光荣而艰巨的任务分给了我,没办法只能硬着头皮上啊,经过几天摸索终于总结出了一套还算安全的APP与服务端通信的机制。

    目录

    1. 防非法调用——身份认证
    2. 防抓包——数据加密
    3. 防重放攻击——时间戳+随机字符串
    4. 防篡改——签名机制
    5. 完整的Java解决方案

    防非法调用——身份认证

    身份认证指只有经过合法授权的用户才能调用我们的接口,这里我们采用的是Token验证机制。

    APP与服务端的整个通信过程如下:

    如何保证APP与服务端通信安全?4个技巧给你的数据加把锁

     

    1. 用户首先需要输入账号密码进行登录;
    2. APP带上用户输入的账号密码请求服务端登录接口;
    3. 服务端校验账号密码,校验成功返回一个唯一Token作为用户身份凭证;
    4. APP将Token缓存,同时登录成功;
    5. 用户使用APP浏览数据,APP每次向服务端请求数据时须同时带上缓存的Token;
    6. 服务端收到请求,首先会校验Token的合法性,校验成功正常返回数据,校验失败直接返回错误;

    Token验证机制解决了什么问题?

    设想一个场景,我们检测到API接口正在被恶意调用,因为所有的接口都必须带Token才能调用,根据Token我们就能快速反查到对应的用户,所以Token验证机制可以帮助我们快速确定调用者的身份。

    发现恶意调用,我们通过Token确定调用者的身份后可以采取Token失效、封禁帐号等措施来阻止恶意调用继续。

    Token验证机制能防止抓包吗?

    Token验证机制并不能防止APP被抓包,因为Token同样存在泄露的风险,恶意调用者只需要带上Token再请求我们的API接口同样还是能获取到数据。

    因为APP与服务端都是明文通信,一抓包就能看到请求参数以及返回数据,所以为了防止被抓包我们必须要对数据进行加密处理。

    防抓包——数据加密

    数据加密的过程,就是对原来明文传输的数据按某种加密算法进行加密处理,使其成为不可读无意义的密文

    加密算法大体上可分为对称加密非对称加密散列算法等几种方式,后面我们的方案都会涉及到。

    对称加密

    对称加密是一种可逆的加密算法,其中“对称”的意思是加密过程和解密过程使用的是同一个密钥。

    如何保证APP与服务端通信安全?4个技巧给你的数据加把锁

     

    常见的对称加密算法有DES、3DES、AES、IDEA等。

    使用对称加密算法一次完整的加解密过程为:

    1. APP与服务端事先约定好对称加密的密钥,各自保存;
    2. APP使用密钥将明文参数加密,再将密文发送到服务端;
    3. 服务端收到密文后使用同样的密钥对密文进行解密操作得到明文;
    4. 服务端同样需要对数据进行加密之后才能返回给APP;

    对称加密算法的特点

    对称加密算法的特点是算法公开、计算量小、加密速度快、加密效率高。对称加密算法的安全性依赖于密钥,任何人只要拿到密钥就能对数据进行加解密操作。

    由于参与通信的双方都需要持有密钥,任何一方的秘钥泄露,那么双方的通信将无安全性可言,所以怎么安全的保存和传递密钥是使用对称加密最需要关注的问题

    非对称加密

    顾名思义非对称加密指的是加密过程和解密过程使用不同的密钥,非对称加密算法需要一对密钥(公钥和私钥),公钥用来加密数据、私钥用来解密数据。

    如何保证APP与服务端通信安全?4个技巧给你的数据加把锁

     

    常见的非对称加密算法有RSA、ECC、ElGamal等。

    非对称加密一次完成的加解密过程为:

    1. APP和服务端各自生成一对密钥对(公钥和私钥),公钥分别给到对方、私钥自己保存;
    2. APP使用服务端的公钥对数据进行加密,然后将加密后的密文发送到服务端;
    3. 服务端收到密文后使用自己的私钥进行解密得到明文数据;
    4. 服务端返回数据同样需要使用APP的公钥对数据进行加密;

    非对称加密算法特点

    非对称加密算法使用公钥加密、私钥解密,私钥不需要公开传输所以安全性较高。同时私钥可以对数据进行签名、公钥可以用来验证签名,可以解决中间人攻击和信息被篡改的问题。

    由于加解密过程使用不同的密钥,所以对大量数据进行加解密运算的话速度是比较慢的,通常情况下非对称加密算法只适合对少量数据进行加解密操作。

    对称加密算法运算速度快但安全性不足、非对称加密算法安全性高但运算速度慢,那么我们的数据加密方案采用哪种加密算法呢?

    既然两种加密算法都有优缺点,那我们可以将两者结合一下:用对称加密算法加解密数据这样可以保证运算速度,用非对称加密算法加密对称加密算法的密钥这样可以兼顾密钥的安全性。

    防重放攻击——时间戳+随机字符串

    数据加密之后再进行通信虽然抓包之后看不到明文数据了,但是这并不能阻止不怀好意之人发起重放攻击。

    拦截到请求之后只需再原样发送该请求到服务端就可以发起重放攻击,如果接口内有一些查库之类的比较耗性能的逻辑,那么在短时间内发起大量重放攻击的话将会直接导致服务端崩溃。

    怎么解决这个问题?

    道理其实很简单,我们只需要保证请求只能被正确处理一次即可,这里我们采用时间戳+随机字符串的解决方案。

    时间戳

    我们在发送的数据里加入当前的时间戳,服务端在收到请求数据后首先取出时间戳与服务器当前时间进行比较,如果两者相差超过一定时间(比如5分钟),那么我们就认为本次请求超时,直接拒绝执行或返回错误就可以。

    随机字符串

    我们在发送的数据中加入一个随机生成的字符串,服务端在收到请求数据后首先在缓存中查找该字符串,如果在缓存中找到则认为这是一次重复请求直接拒绝处理,否则将该字符串加入缓存并继续执行正确逻辑。

    在请求中加入时间戳与随机字符串之后,服务端收到请求后会首先对时间戳和随机字符串进行校验,校验通过才会执行正常的业务处理逻辑。

    防篡改——数字签名

    为了防重放攻击,我们在数据中加入了时间戳与随机字符串,但是别人在拦截到我们的请求之后也可以对时间戳和随机字符串进行篡改,面对这种情况服务端要怎么分辨呢?

    为了防止数据在传输过程中被篡改,我们引入数字签名机制。

    信息摘要算法

    信息摘要算法(或者叫散列算法)是一种不可逆算法,任意长度的明文数据经过信息摘要算法计算后都可以得出一个固定长度的值(签名)。

    常见的信息摘要算法有MD5、SHA-1等。

    详细的签名过程:

    如何保证APP与服务端通信安全?4个技巧给你的数据加把锁

     

    1. 将数据密文、时间戳、随机字符串以及私密的加盐值一起进行信息摘要算法计算得到签名值;
    2. APP将数据密文、时间戳、随机字符串以及签名值一起发往服务端;
    3. 服务端收到数据后对数据密文、时间戳、随机字符串以及加盐值一起进行同样的信息摘要算法计算,将计算出的签名与数据中签名进行对比,签名一致证明没有没有被篡改;

    为什么进行信息摘要计算要“加盐”

    举个例子就明白了,比如说123经过MD5计算后的签名值是abc,那么就会产生123->abc这样的对应关系,看到签名值abc我就能反查到原值为123。如果有人收集并保存了足够多的这种对应关系,那么就有可能从签名值反推出原值。

    这个时候加盐操作就派上了用场,首先我们生成一个加盐值qwe,这个加盐值qwe并不会在网络传输,只有通信双方自己知道。

    我们不直接计算123的签名值,我们将加盐值附加到123的后面得到123qwe,接着我们对123qwe进行MD5计算得到一个不一样的签名值def。

    所以说即使原值一样,但只要加盐值不同那么最后得到签名值就不一样,这样也就无法从签名值反推出原值了。

    完整的Java解决方案

    因为我主要搞Java开发,所以就用Java语言实现了一套加解密方案,对称加密采用AES算法、非对称加密采用RSA算法,信息摘要算法采用MD5算法。

    完整代码执行流程:

    如何保证APP与服务端通信安全?4个技巧给你的数据加把锁

     

    下面分步骤进行详细介绍:

    准备工作:APP与服务端各自生成一对RSA密钥对(公钥和私钥),公钥给到对方、私钥各自私密保存;

    APP发送加密数据流程

    1、生成一个随机的AES算法密钥;

    2、使用服务端的RSA公钥对AES密钥明文进行加密得到AES密钥密文;

    3、对参数明文进行AES加密得到参数密文;

    4、生成当前请求时间的时间戳;

    5、为该次请求生成一个随机字符串;

    6、将参数密文、时间戳、随机字符串和AES密钥密文进行MD5计算得到md5值;

    7、使用APP自己的RSA私钥对md5值进行签名得到签名值;

    8、将参数密文、时间戳、随机字符串、AES密钥密文和签名值一起发送到服务端;

    服务端解密数据流程

    1、校验时间戳与服务器当前时间的差值是否在合理的区间,超过则认为该次请求超时;

    2、校验随机字符串是否已经在缓存中,如果已经在缓存中说明该次请求为重复请求,否则将该字符串加入缓存;

    3、从收到的数据中取出参数密文、时间戳、随机字符串和AES密钥密文进行MD5计算得到md5值;

    4、使用APP的RSA公钥对计算得到的md5值和请求数据中的签名值进行验证,签名验证通过则说明请求数据没有被篡改;

    5、服务端使用自己的RSA私钥解密AES密钥密文得到AES密钥明文;

    6、使用AES密钥明文对参数密文进行AES解密操作得到参数明文;

    7、拿到参数明文之后进行正常的业务处理逻辑;

    8、服务端数据需要经过同样的加密操作之后才能返回给APP;

    总结

    APP与服务端肯定是要使用HTTPS协议进行通信的,再搭配上RAS+AES混合加密算法以及数字签名机制,相信这套方案在绝大部分情况下是可以保证通信及数据安全的。

    当然了,不排除APP有被人破解的可能,这种情况下任何加密机制都是白搭。但是不能说我们的加密机制就没用了,我们只需要将破解我们APP的成本提高到一定程度就可以了。

    这个道理其实就跟门锁一样,市面上绝大部分门锁只要有时间都可以被开锁的人打开,那你能说门锁就没有存在的意义了吗?

    展开全文
  • 那么,服务器如何识别客户端的身份?我们如何保证数据传输过程中的安全性?要靠两个东西:使用AppKey做身份识别,使用AppSecret校验数据。 这两个东西的定义可以参考淘宝开放平台上这种比较严肃的说法: AppKey ...
  • APP与智能手表是如何通信的 1. Android 与服务器通信方式主要有两种,一种是http 通信 ,一种是socket 通信。 两者的最大差异在于,http连接使用的是“请求—响应方式”,即在请求时建立连接通道,当客户端向...
  • 最严重的问题就是我们的APP与服务器通信接口没有加密处理被人抓包了,有人非法请求我们的接口获取数据。怎么处理这个问题呢?领导又把这个光荣而艰巨的任务分给了我,没办法只能硬着头皮上啊,经过几天摸索终于...
  • 原文: http://www.devx.com/wireless/Article/43551在上一章,我们讨论了如何在iPhoneapp中调用web服务以及如何解析服务器返回的XML。在Web服务大行其道的当今,调用web服务的代价是高昂的,尤其是你仅仅是抓取少量...
  • 这篇文章主要为大家详细介绍了客户端(vue框架)与服务器(koa框架)通信服务器跨域配置分析,具有一定的参考价值,可以用来参考一下。感兴趣的小伙伴,下面一起跟随512笔记的小编两巴掌来看看吧!本篇博客主要说明:...
  • 本篇博客主要说明:前后端框架(本例中是vue和koa)如何发送请求?获取响应?以及跨域问题如何解决?vue部分:import App from './App.vue'import Axios from 'axios'new Vue({el: '#app',render: h => h(App),...
  • 上一篇文章写了如何通过Java层实现Socket和服务器的Socket进行通信,这一篇继续深究,写个如何通过native层实现socket和服务器进行通信服务器端代码和前一篇博客代码一致,主要看下Android端的代码。首先看下Main2...
  • 前后端框架(本例中是vue和koa)如何发送请求?获取响应? 以及跨域问题如何解决? vue部分: import App from './App.vue' import Axios from 'axios' new Vue({ el: '#app', render: h => h(App), mounted(){...
  • 1、前言 一般 iOS 开发者做 App 开发大部分时候都是通过 Http(s) 请求跟后台服务器打交道,做一些信息展示和用户交互。很少涉及到去跟外部硬件设备连接的开发。...本文就针对 iOS 的 App 如何跟外部设备...
  • 公司的 mobile app 是外包给其他公司做的,所以现在他们需要我们提供 API 接口进行调试,由于没有 API 开发的经验,所以现在一个比较难把握的问题就是如何实现服务器移动 APP 端通信时的用户身份认证...
  • 使用TCP/IP与服务器进行通信

    千次阅读 2013-11-14 11:09:16
    转载:... ...在上一章,我们讨论了如何在iPhoneapp中调用web服务以及如何解析服务器返回的XML。在Web服务大行其道的当今,调用web服务的代价是高昂的,尤其是你仅仅是抓取少量数据的
  • 而针对APP服务端的性能测试,主要关注点在于服务端的压力,传统软件的服务端性能测试没太大区别,都是根据客户端服务端通信使用的不同协议来构建对应协议的请求,目前使用最多的还是http协议。性能测试中的脚本...
  • 之前写过一个最基本的TCP客户/服务端交互示例:手机上的APP如何与服务器通信的,流程如下: 流程为:1、客户端从标准输入读入一行文...

空空如也

空空如也

1 2 3 4 5 ... 12
收藏数 239
精华内容 95
关键字:

服务器如何与app通信