`怎么算呢`标签下的文章

编程

扫码登录(3)

Context

Http是在tcp的基础上做了高级封装,变成了一个被动的,短链接请求.请求完了就把连接断开了,下一次要请求,就需要再一次进行tcp握手,然后通信……在前面第一篇中,我用了定时轮训的方式问服务器,当前这个二维码是否登录,从功能的角度来说,应该是没有问题的,而且写起来也是特别的方便.

不过仔细的想一想,其实还是比较浪费资源的.从信息论的角度来说,中间的N次质询,相当于没有任何的信息量(请不要深究这一句),所以,我们不用http来更新扫码状态!

我们用

Web Socket!

Web Socket

关于websocket的定义,解释什么的,百度一下,你全都知道了,所以我就不在这个地方多说.

只说明一点,websocket屌的飞起,和socket的操作一毛一样,关键是服务器终于可以主动的发消息给客户端啦!

他算是一个长连接,握手之后,就开始了肮脏的通信,通信完了,一方可以发起关闭操作,然后……,最后连接就拆掉了.

Django

websocket怎么说也就只能算是对web服务器的一个扩充,如何在已有的web服务器上快速集成一个websocket的服务,才是比较重要的~

由于之前用的是django做服务器,所以我们就找找给django加上websocket的方法~

在他的官方文档中提到了很多可用的开源app,其中有一个叫做channels的库,他用了asgi,直接可以快速集成在django中,牛的一批.

Channels

这个库的文档,真的是无力吐槽,我真是集成的很辛苦

我们来简化一下他的文档吧!

Router

和django一样,websocket的连接也要有个路由的,我们先建立一个py文件

from channels.routing import route
# 这个文件等会儿解释
from users.webClient import connection_handler, disconnection_handler,subscribe_token

channel_routing = [
    route("websocket.connect", connection_handler, path=r"/ws/$"),
    route("websocket.disconnect", disconnection_handler, path=r"/ws/$"),
    route("websocket.receive", subscribe_token, path=r"/ws/$"),
]

这里的websocket.connect,websocket.disconnect,websocket.receive相当于是websocket的三种事件,已连接、已断开、收到消息

中间那个参数和django里面的那个描述一样,就是该路由对应的处理方法.

Handler

现在来解释一下上一节说的 users.webClient这个文件

# -*- coding: utf-8 -*-

import json
from channels.sessions import channel_session
from users.qrcode_helper import *


@channel_session
def connection_handler(message):
    message.reply_channel.send({"text": json.dumps({"status":False,"msg":"connected"})})


@channel_session
def subscribe_token(message):
    try:
        obj = json.loads(message.content["text"])
        tokens = obj["token"]
        qrcode_record = fetch_qrcode_record_with_token(token=tokens)
        if qrcode_record is not None:
            if is_token_pass_the_authentic(tokens):
                message.reply_channel.send(
                    {"text": json.dumps({"status": True, "msg": "succeed"})})
            else:
                message.reply_channel.send(
                    {"text": json.dumps({"status": False, "msg": "succeed"})})
                qrcode_record.webSocket_session = str(message.content["reply_channel"])
                qrcode_record.save()
                # link token to this channel
        else:
            message.reply_channel.send({"text": json.dumps({"status": "-1", "msg": "token invalid"})})
            message.reply_channel.send({"close": True})
    except Exception as e:
        message.reply_channel.send({"text": e.message})
        message.reply_channel.send({"close": True})


@channel_session
def disconnection_handler(message):
    channel = str(message.content["reply_channel"])
    qrcode_records = fetch_qrcode_record_with_web_socket_channel(channel)
    if qrcode_records is not None:
        for qrcode_record in qrcode_records:
            qrcode_record.webSocket_session = ""
            qrcode_record.save()

很长,也不用仔细看,因为里面主要做了扫码登录里面的事情,如果只是要集成一下websocket的话,不用关心这些方法里面的操作,只要注意这里每个方法的传入参数 message

Message有那些用处呢?

1.通过message.channel或者message.reply_channel可以获取到连接的通道(请暂时不要使用高级操作)
2.message.content其实是一个字典,这个字典遵循了ASGI的消息模型,里面有text,reply_channel等等字段

在我的处理代码里面,我从message.content里面取出了reply_channel,他的value是一个长得很奇怪的字符串(我把它类比成文件描述符把),毕竟要有很多很多很多连接的话,它们的文件描述符不能重复对吧,所以就又长又难看,比如像daphne.response.FSITXWDDzG!IOtDNILqzh

有了这个reply_channel,在后面主动发送消息下去的时候,就可以指定连接发送了~

Attention

注意,这里的websocket.connect的handler里面一定一定,千万千万要给反馈消息回去,不然浏览器(只测试了Chrome的)会以为连接超时的!然后给你一个503 bad gateway,之后就主动断开连接了,留你一脸蒙逼的在那儿傻看着.

Send

现在我们可以看看之前第一章中的app调用的扫码接口了

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        if qr_record.webSocket_session is not None:
            # send new status to the web socket
            channel = Channel(name=qr_record.webSocket_session)
            content = {"text": json.dumps({"status": True, "msg": "succeed"})}
            channel.send(content)
            pass
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里加了一些些小的改动,我再把他取出来

if qr_record.webSocket_session is not None:
    channel = Channel(name=qr_record.webSocket_session)
    content = {"text": json.dumps({"status": True, "msg": "succeed"})}
    channel.send(content)

websocket.receive的时候,我把websocket的文件描述符记录在了qrcode的记录中,这样扫描某一个qrcode的时候,就可以知道这个二维码是否有websocket正在订阅他,有订阅的话,就可以拿来对指定的通道进行发送了

End

网页端真的没什么好说的,直接贴(里面没有很严密的控制权限,稍微改一改就行,但是我懒)

function listen_for_status() {
    socket = new WebSocket("ws://" + window.location.host + '/ws/');
    socket.onmessage = function (e) {
        var obj = JSON.parse(e.data);
        if (obj.status == true) {
            socket.close();
            window.location="dashboard.html";
        }
    };
    socket.onopen = function () {
        var obj = {};
        obj.token = token;
        socket.send(JSON.stringify(obj));
    };
}

还有不要忘记更新nginx的配置

扫码登录的这个小玩意儿,虽然看起来很是简单,但是也耗了我很多的时间,这个过程中,也是学到了很多新的东西呢!

这次也是达成了一个新的成就:WebSocket(Server&Client)

阅读剩下更多

编程

扫码登录(2)

Context

几天前把扫码登录的服务器总结了一下,由于时间太晚,篇幅太长,而且涉及的内容是两个板块的,所以就没有在那一篇文章中写app端的一些实现。

事实上,app这里并没有简单多少,主要的难点大概就是扫码这里吧~之前虽然是在Alinone中做过扫码的功能,但是当时使用了cocoapods里面的一个库,所以没有怎么深入的理解AVFoundation。这次,我就偏偏不要用开源库,读读官方文档,把AVFoundation的一些功能搞搞明白。

How does it work

好好研究了官方文档一番,我大概明白了一件事儿,整个视频流的工作过程是由一个会话(AVCaptureSession)来控制的,这个会话有输入(AVCaptureInput)和输出(AVCaptureOutput)

当session开始工作的时候,数据就从输入经过会话传到了输出,当然这里说的全都是抽象类,具体到这个app里面的话,输出就是AVCaptureOutput的一个子类AVCaptureMetadataOutput

这玩意儿就是专门用来解析视频流信息,提取其中的metadata的,其中包括BarCode、QRCode等其他code~牛逼的很

Make it

这次没有用什么传说中的设计模式来写这个demo,因为这样会有点儿小题大做的感觉,直接在VC中定义了一切。

@interface MainViewController ()<AVCaptureMetadataOutputObjectsDelegate>
{
    AVCaptureVideoPreviewLayer *previewLayer;
    dispatch_queue_t avProcessQueue;
    IBOutlet UIView *scan_view;
    AVCaptureSession *avCaptureSession;
    AVCaptureDevice *avDevice;
    AVCaptureDeviceInput *avInput;
    AVCaptureMetadataOutput *metaOutput;
    NSMutableArray *capturedList;
}
@property (nonatomic, weak) IBOutlet UIButton *scanCodeButton;

@end

注意这里有个avProcessQueue(dispatch_queue_t),因为视频处理是比较消耗资源和时间的,所以如果让他们在主线程中运行的话,必死,所以要单独搞一个线程出来让它们运行。

AVCaptureVideoPreviewLayer是CALayer的一个子类,可以方方便便的把AVSession里面的内容显示出来,如果没有这个东西的话,就很难想象如何把摄像头捕捉到的东西显示在界面上了~

capturedList是一个用来扫描结果的纪录列表,后面会解释他存在的意义。

Configure AVFoundation Elements

- (void)viewDidLoad {
    [super viewDidLoad];
    // 建立一个新的队列(线程)
    avProcessQueue = dispatch_queue_create( "session queue", DISPATCH_QUEUE_SERIAL );

    // 初始化设备
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // 初始化Session
    avCaptureSession=[[AVCaptureSession alloc]init];
    [avCaptureSession setSessionPreset:AVCaptureSessionPresetHigh];

    // 初始化Input
    NSError *error;
    avInput=[[AVCaptureDeviceInput alloc]initWithDevice:device error:&error];
    [avCaptureSession addInput:avInput];

    // 初始化Output
    metaOutput =[[AVCaptureMetadataOutput alloc]init];
    [metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    [avCaptureSession addOutput:metaOutput];
    metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

    // 初始化预览layer
    previewLayer=[AVCaptureVideoPreviewLayer layerWithSession:avCaptureSession];
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    [scan_view.layer addSublayer:previewLayer];
    previewLayer.frame=CGRectMake(0, 0, 250, 250);

}

这里有一个超级超级超级大的坑,我找了大半天才知道原因

metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

就是这个玩意儿,无论我怎么写,都会直接crash,说什么output不支持这种识别方式(我都删的只剩下一种了诶,同学!),结果原来是这个扫描类型的设置必须在该output被加入了session才可以设置,不然就会直接挂掉……也就是

[avCaptureSession addOutput:metaOutput];

这个之后才可以有效的设置识别类型

Please Allow Me To Control Your Camera

不过,摄像头是一个比较敏感的东西,涉及到了隐私,所以在某一代系统之后,AVFoundation要获取视频流是需要权限才行的~

switch ( [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] )
{
    case AVAuthorizationStatusAuthorized:
    {
        // 以前就获取到权限了!
        break;
    }
    case AVAuthorizationStatusNotDetermined:
    {
        // 还不知道有没有获取到权限,我们来问一问
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) {
            // 索取权限完毕,处理结果
        }];
        break;
    }
    default:
    {
        // 以前就没有获取到权限!
        break;
    }
}

这里已经注释的很清楚了,直接在适当的位置复制粘贴进去就好啦!

I got it

在前面也看到了

[metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

这一行设置,意思是AVCaptureMetadataOutput扫描到有效的信息之后,会通过代理的形式调用一个回调函数(方法),顺便把扫描结果给返回出来,所以这里我就让我的MainViewController遵从了AVCaptureMetadataOutputObjectsDelegate协议,并且实现了接口需要的方法

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count>0) {
        for (AVMetadataObject *data in metadataObjects){
            if ([data isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) {
                AVMetadataMachineReadableCodeObject *result=(AVMetadataMachineReadableCodeObject*)data;
                if (![capturedList containsObject:result.stringValue]) {
                    // 这里出现了capturedList!
                    [capturedList addObject:result.stringValue];
                    // 扫描的Raw内容就是这么简单
                    NSLog(@"scanned code is %@",result.stringValue);
                    [NetworkHelper permit:result.stringValue andSucceed:nil andFail:nil];
                }
            }
        }
    }
}

好了,现在可以说明一下,为什么要用capturedList啦~

因为,这个- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection方法是不会中断的,是会在每个周期内都执行的(只要摄像头下有符合要求的Code),

换句话说,就是如果你的摄像头指着一个QRCode,系统就会使劲的调用这个代理方法,假设没有做任何的判断,那么很有可能对一个QRCode的处理会在一瞬间做N多次,在例子中我用到了一个网络请求,如果没有加前面的那个判断的话,服务器可能会哭~(哈哈)

End

好像只要这么多就可以把扫码功能搞定了呢!我当年为啥要偷懒去用开源库呢~

(要不然我也把这个东西整理一下,开个源,骗一些Star?)

阅读剩下更多

编程

扫码登录(1)

Context

前天睡醒之前,隐隐约约的梦到了扫码登录的实现方案,虽然这个扫码登录已经有很多很多很多人实现过了,而且说不定还有了框架(我没有搜索过),但是既然我在梦里想过了一遍,那还是不要辜负自己,把它做出来看看吧~

Requirements

扫码登录:网页上显示个二维码,用登录过的app扫描一下,然后网页上就显示登陆成功

所以需要: 网页一个、app一个、服务器一个

网页用了jqueryjquery-qrcode两个框架.以前做艾米奇定位鞋的时候,用到了很多的二维码,当时把二维码图片文件存在了服务器的本地,我觉得比较难过,因为这次我没有服务器,所有的一切都在我的小电脑中,所以这次就打算让客户端自己去根据字符串生成QRCode了

服务器依然是Django,用了一下Celery(我假装做了一个发送短信验证码的异步操作),Redis(Celery需要的).只配了nginx,没有配uwsgi,因为我想动态的看看服务器运行起来的一些log,用了uwsgi我就不方便调试了~

App嘛,必然又是iOS,我做得快一些~,用了几个很基础的框架,AFNetworking、IQKeyBoardManager

Solution in Dream

在梦里,我是打算在服务器中建立QRCode和一个session的关系,然后App登录过的Session可以将QRCode对应的Session也变为已登陆状态,然后网页端的Session就变成了已登录状态,然后就扫码登录成功了.

For Security

在整套工作过程中,哪些部分可能会出现安全风险呢?

要分析这个问题,可能需要先整理一下,什么情况下,网页端可以登陆成功.下面是根据功能得到的最显而易见的一组条件

1. 网页端有QRCode
2. App扫描了QRCode,并且同意登陆

这意思是,网页问服务器要一个QRCode,然后在那儿一个劲的问服务器,我这个QRCode通过登录了嘛?

App扫描了QRCode,告诉服务器,我是XXX,我扫描了这个QRCode,我要登录

然后服务器就告诉了网页端,你登录了

这样的做法,我觉得大概也是可行的,只不过QRCode这东西可能攻击者伪造一下,就撞到别的已经通过的QRCode,然后他就幸运的登录成功了呢!

所以我想,应该还是要多搞一些规则才行~

然后就建立了这样一个模型

class LoginQRCode(models.Model):
    # 显示的二维码
    code = models.CharField(max_length=255)
    # 传递参数时必备参数
    token = models.UUIDField()
    # session中对应的uuid
    session_token = models.UUIDField()
    # 创建时间(更新时间)
    timestamp = models.DateTimeField()
    # 登陆后记录一下这个二维码对应的用户
    user_id = models.UUIDField(blank=True, null=True)
    # 是否通过
    status = models.BooleanField(default=False)

    def get_fetch_qrcode_response(self):
        return {"code": self.code, "token": str(self.token), "timestamp":get_update_time(self.timestamp)}

在获取QRCode的时候

def ask_for_login_qrcode(request):
    if "login" in request.session:
        if request.session["login"] is True:
            return JsonResponse({"msg": "already login", "status": 1}, status=200)
    if "session_uuid" in request.session:
        old_uuid = request.session["session_uuid"]
        old_qrcode_record = fetch_qrcode_record_with_session(old_uuid)
        if qrcode_record_is_expired(old_qrcode_record) is True:
            record = generate_new_qrcode_record_for_request(request)
            return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)
        else:
            return JsonResponse({"msg": "succeed", "status": 0, "data": old_qrcode_record.get_fetch_qrcode_response()},
                                status=200)
    else:
        record = generate_new_qrcode_record_for_request(request)
        return JsonResponse({"msg": "succeed", "status": 0, "data": record.get_fetch_qrcode_response()}, status=200)

意思差不多就是,一个session会产生一个token,一个code,一个session_token,其中session_token会对应记录在session中,以便服务器根据session来判断某个token是否是该会话产生的,(也算是防了一下跨站攻击?),app根据扫描二维码,得到code,将code作为参数告诉服务器,我要这个code登录,然后服务器对这个qrcode纪录进行授权,之后检查到这个session的时候,将这个session标记为已登录,整个流程就走完了~

所以处理App扫码之后做的请求是

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里用了两个自己写的修饰器用了确定session是登录过的,并且包含了参数”code”~

当然还有很多个工具方法,看名字大概也知道他是什么意思吧~

最后是刷新登录状态的接口

@csrf_exempt
@require_POST
@require_parameter(["token"])
def checking_login_status(request):
    token_uuid = request.POST["token"]
    qrcode_entity = fetch_qrcode_record_with_token(token_uuid)
    if qrcode_entity is None:
        return JsonResponse({"msg": "bad request", "status": -403}, status=400)
    if "login" in request.session:
        if request.session["login"] is True:
            if request.session["session_uuid"] == qrcode_entity.session_token:
                return JsonResponse({"msg": "pass", "status": 0}, status=200)
            else:
                return JsonResponse({"msg": "token error", "status": -1}, status=403)
        elif qrcode_entity.status is True and request.session["session_uuid"] == str(qrcode_entity.session_token):
            request.session["login"] = True
            request.session["user_id"] = str(qrcode_entity.user_id)
            return JsonResponse({"msg": "pass", "status": 0}, status=200)
        elif qrcode_record_is_expired(qrcode_entity):
            return JsonResponse({"msg": "code is expired,please refresh it", "status": -1}, status=200)
    return JsonResponse({"msg": "waiting", "status": 1}, status=200)

这段我也懒得解释了,反正要改了……

Have a break

这一篇先写这么多,下一篇会讲扫码App的故事(网页端会在很后面讲,因为这个版本网页端和服务端存在一个轮训操作,这个操作效率很低下,我打算在后面加入了websocket之后,再来一起讲网页端~)

阅读剩下更多

编程

Socket

Context

这个故事从三年前开始说起.

那时老师让我搞Android的一个防伪项目(精益防伪),就是那个用Android的NFC功能来鉴别一个酒是否是假冒伪劣的,也是那个项目让我认识了卢师兄(在华为可厉害了呢).

那时的我只用过http来进行网络请求,没有用过socket来进行数据交互.

由于项目需要,那时候就临时在Android上搞了一波socket通信.

第一次做Android就要碰Socket还是蛮困难的(虽然我已经搞了两年iOS,但是iOS和Android毕竟还是不一样的,而且我还是个孩子),因为界面没做过,java也不是很熟悉,甚至连Android的手机都没有用过. 由于时间比较紧张,所以界面就请望神帮我画了,我去写了数据处理和Socket通信. 那也是第一次我知道了Android上面有很严格的线程限制. 在iOS上的主线程中调用网络,会堵塞,卡在那儿,等到请求完了,就会恢复;不过在android上面,主线程碰一下网络竟然就异常了~(真凶)

Socket

Socket其实不应该和http分开说,因为http只是socket的一种扩展罢了,要进行网络通信,总是离不开Socket的.http只不过是在socket上面进行了很多复杂的封装罢了(很复杂).

socket,翻译过来就是,插座.打个比方就是,我要和你收发数据,就把数据线插到你的插孔里面,然后我们就在这根线里面交流.一个网络节点可以有很多的插座孔,所以也就可以同时和很多很多人一起交流信息.而http是插上了插座,发一句话,收一句话,然后就把插头拔掉了(连接结束了)好处就是服务器和客户端不用一直维护一个连接,缺点嘛,这是一个短连接,下一次想要进行网络请求还需要把TCP的握手流程走一遍,而且是单向操作(只能客户端找服务器麻烦,服务器永远无法主动找客户端麻烦)

Now

三年前的旧事讲完了,该说说这次的故事了

这次我们的产品中会涉及用到socket传输文件,这比三年前那个要求更高了一些,因为文件还是蛮大的,之前每次的数据也就十几个字节(128位)(NFC标签儿本来也存不了多少内容)

Server

先从服务器开始说起吧!由于这个socket传输只会一对一的传输,所以这个服务器根根本本不用考虑并发和异步什么的东西,所以

就用Python做个简单的socket服务器吧!

code
import threading
import SocketServer
import datetime
import time

BUFFER_SIZE = 4096


class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
    t = None
    wantDisconnect = False

    def handle(self):
        if self.t is None:
            self.t = datetime.datetime.now()
            time_interval = 0
        else:
            time_interval = (datetime.datetime.now() - self.t).seconds
        while time_interval < 60 and not self.wantDisconnect:
            data = self.request.recv(BUFFER_SIZE)
            if data:
                self.t = datetime.datetime.now()
                print "prepare to send file"
                f = open(str(data), 'rb')
                l = f.read(BUFFER_SIZE)
                while l:
                    self.request.send(l)
                    print("sending (%x)", l)
                    l = f.read(BUFFER_SIZE)
                    time.sleep(1/20)
                time.sleep(1)
                self.request.sendall("\nsending ok")
                f.close()
            else:
                print threading.currentThread().name, " no data"
                break

    def setup(self):
        print threading.currentThread(), " start"
        self.t = datetime.datetime.now()

    def finish(self):
        self.wantDisconnect = True
        print threading.currentThread(), " end"


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


HOST, PORT = "0.0.0.0", 55555

server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
server.serve_forever()

我直接在服务器的文件目录下放了几个图片文件用来做测试的~

这差不多基本上是直接从Python的SocketServer文档中抄来的

一个多线程的socket服务器就是这么方便

其中setup()finish()没什么太大的意思,反正就是又一个socket连接进来了和对方主动断开后会做的事情

关键的操作都在handle中!

先贴一下官方的解释:

handle()

    This function must do all the work required to service a request. The default implementation does nothing. Several instance attributes are available to it; the request is available as self.request; the client address as self.client_address; and the server instance as self.server, in case it needs access to per-server information.

    The type of self.request is different for datagram or stream services. For stream services, self.request is a socket object; for datagram services, self.request is a pair of string and socket.

就是说有请求进来了,就会在这里中断 ,aka “回调”(但我觉得用中断好像更加形象生动)

self.request将获取到socket实例,有了这个socket实例我们就可以读读写写了

这里的代码是传入的数据代表的是文件,服务器根据文件名,把数据回传回去,就是这么简单,看看效果图

效果图

密集恐惧症了吧?哈哈哈~在服务器把发送的数据打印出来,可以方便调试,(肉眼)检查数据有没有收完收对……

等下,time.sleep(1/20)这个东西有啥子用处?

我担心发送的太快接收端粘包,所以就稍微给一点点儿延迟,缓解一下压力,不过实测下来貌似是我多虑了~

iOS

以我的尿性,客户端必然是从iOS开始搞的~

目标很简单,连接服务器,发个文件名,一个劲的收数据,最后显示效果

iOS效果

略~

Doing

多年前在CocoaPods上面看到了CocoaAsyncSocket,今天终于要动手搞一搞了!好激动

一般大家在github的README.md里面都要把自己的库的用法好好的解释一遍,让访问者眼前一亮,然后送出Star,不过这个库真牛逼,README.md里面装了整整一页的逼!

然后我去wiki里面看,竟然还给了好多个链接,一时半会儿还找不到他的使用方法,不过我看了CommonPitfalls这个板块,我觉得写的很屌,如果我前面的那些关于socket的废话你没看懂的话,可以去这个链接里面再看看.里面讲了不少误区,坑,雷……

好吧,其实它真正的用法在Intro_GCDAsyncSocket里面

教了连接动作,连接结果,读操作,写操作

贴代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    socket=[[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    NSError *error;
    buf=[NSMutableData data];
    [socket connectToHost:@"192.168.199.131" onPort:55555 error:&error];
}

-(IBAction)send_action:(id)sender{
    [socket writeData:[self.commandField.text  dataUsingEncoding:NSASCIIStringEncoding] withTimeout:5.f tag:12345];
}

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"socket is connected");
    [self.sendButton setEnabled:YES];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"[%ld]Did receive Completely %@,(%ld)",tag,data,data.length);
    self.console.text=[NSString stringWithFormat:@"%@\n%@",data,self.console.text];
    UIImage *image=[UIImage imageWithData:data];
    self.imageView.image=image;
}

- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag{
    NSLog(@"[%ld]receive (%lu)*1024 bytes",tag,partialLength);
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    if (tag==12345){
        [sock readDataToData:[@"\nsending ok" dataUsingEncoding:NSASCIIStringEncoding] withTimeout:60.f tag:119];
    }
}

- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock{
    NSLog(@"socket closed");
    [self.sendButton setEnabled:NO];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
    NSLog(@"socket disconnect");
    NSLog(@"buf is %@",buf);
    [self.sendButton setEnabled:NO];
}

这代码好像蛮长的~

其中有partial的回调方法都是接收不完整的时候会做的事儿

Socket本来就是全双工的,就是可以一边儿发送一边儿接收,不过这现在给我做成了个半双工的了,所以我在读取的时候设置了一个结束条件\nsending ok,接收到了这个的时候,就会停止.

Android

android上面我想装逼,不用框架,直接用socket来撸,写的还是蛮痛苦的

先只贴代码,先不做解释了,因为,写的不是特别好……等到优化好了,再写一篇新的,好好解释一番~

先搞个center

public class SocketCenter {
    SocketEntity socket;
    Handler handler = new Handler();
    Thread socket_thread;
    Handler network_handler;
    public void setup() {
        EventBus.getDefault().register(this);
        socket_thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                network_handler=new Handler();
                Looper.loop();
            }
        });
        socket_thread.start();
    }
    public void connect_server() {
        network_handler.post(new Runnable() {
            @Override
            public void run() {
                socket = new SocketEntity("192.168.199.131", 55555, handler);
                socket.connect();
            }
        });
    }
    public void disconnect() {
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(SocketEvent event) {
        if (event.getName().equalsIgnoreCase(EventConstant.Socket_Connected)) {
            network_handler.post(new Runnable() {
                @Override
                public void run() {
                    socket.ask_for_file("img1.jpg", "\nsending ok");
                }
            });
        }
    }
}

再把实例搞出来

public class SocketEntity {
    private Socket socket;
    String address;
    int port;
    BufferedReader reader;
    BufferedWriter writer;
    byte[] temp_data;
    ArrayList<byte[]> received_queue;
    WeakReference<Handler> handler_reference;
    Thread async_thread;
    SocketEntity(String address, int port, Handler handler) {
        socket = new Socket();
        handler_reference = new WeakReference<Handler>(handler);
        this.address = address;
        this.port = port;
        received_queue = new ArrayList<>();
        async_thread = new Thread();
    }
    Socket getSocket() {
        return socket;
    }
    void connect() {
        Log.e("SocketUtil", "Start Connect");
        if (socket != null) {
            if (!socket.isConnected()) {
                try {
                    socket.connect(new InetSocketAddress(address, port), 5000);
                    writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                    reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    SocketEvent event = SocketEvent.connectEventInstance();
                    EventBus.getDefault().post(event);
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-200);
                    EventBus.getDefault().post(event);
                    writer = null;
                    reader = null;
                }
            }
        }
    }
    void disconnect() {
        if (socket != null) {
            if (socket.isConnected()) {
                try {
                    socket.close();
                    writer.close();
                    reader.close();
                    writer = null;
                    reader = null;
                } catch (IOException e) {
                    e.printStackTrace();
                    SocketEvent event = SocketEvent.failBaseEventInstance(-400);
                    EventBus.getDefault().post(event);
                    if (!socket.isConnected()) {
                        writer = null;
                        reader = null;
                    }
                }
            }
        }
    }
    void ask_for_file(String filename, String endPoint) {
        send_command(filename);
//        receive_data_until(endPoint.getBytes());
        receive_data(endPoint.getBytes());
    }
    private void receive_data(byte[] endpoint) {
        try {
            char[] buf = new char[1024];
            int total = 0;
            boolean isEnd = false;
            String s = "";
            while (!isEnd) {
                int length = reader.read(buf, 0, 1024);
                isEnd = (length == -1);
                if (length > 0) {
                    char[] valid_data = Arrays.copyOfRange(buf, 0, length);
                    String string = String.valueOf(valid_data);
                    s=s+string;
                    isEnd = string.contains(new String(endpoint));
                    if (!isEnd) {
                        total += length;
                    } else {
                        total += length;
                        total -= endpoint.length;
                    }
                    Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(string.getBytes())));
                    Log.e("String is ", string);
                }
            }
            Log.e("Socket_Received", "Total Size is " + total + " bytes");
//            temp_data=s.getBytes();
            temp_data=Arrays.copyOfRange(s.getBytes(),0,total);
            Log.e("Socket_Received", "Total binary Size is " + temp_data.length + " bytes");
            SocketEvent event= SocketEvent.fileReceivedEventInstance(temp_data);
            EventBus.getDefault().post(event);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void receive_data_until(byte[] ends) {
        try {
            byte[] temp = null;
            byte[] temp_buffer = null;
            while (ends != temp) {
                String line = reader.readLine();
                byte[] bytes = line.getBytes();
                Log.e("Socket_Received", String.valueOf(HexUtil.encodeHex(bytes)));
                int length = ends.length;
                if (bytes.length >= length) {
                    temp = Arrays.copyOfRange(bytes, (bytes.length - length), bytes.length);
                    temp_buffer = line.getBytes();
                } else if (temp_buffer != null) {
                    int rear = bytes.length;
                    int front = ends.length - rear;
                    if (temp_buffer.length > front) {
                        temp = new byte[ends.length];
                        for (int i = 0; i < front; i++) {
                            temp[i] = temp_buffer[temp_buffer.length - front + i];
                        }
                        for (int i = 0; i < rear; i++) {
                            temp[front + i] = bytes[i];
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void send_command(byte[] command) {
        String cmd = Arrays.toString(command);
        send_command(cmd);
    }
    private void send_command(String command) {
        if (socket != null && socket.isConnected()) {
            try {
                Log.e("SocketUtil", "Send " + command);
                writer.write(command);
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
                SocketEvent event = SocketEvent.failBaseEventInstance(-1);
                EventBus.getDefault().post(event);
            }
        }
    }
}

这里面有很多的问题,不过还是成功的做到了连接,发送指令,接收数据,由于最后BitmapFactory.decodeByteArray存在一些问题,估计和接收的数据有关系,所以没有效果图……(难过)

End

先写这么多了,等我下次优化完了,说不定就会有更深入的理解,更好的代码,更加出色的性能,到时候再贴出来留作纪念!

阅读剩下更多

返回顶部