大体思路
client通过tcp向server传输一个待识别的验证图片,server端识别后通过tcp回传给client。
server端识别验证码采用pytesseract。
环境:server/python3.5/Ubuntu1604,client/Qt5.3/win7。
1、文件传输
tcp很多人都很熟悉,经常用来传递数据。但如何用于传递文件呢。尝试过的同学可能知道常见的有这么几个问题:
1)文件较大时tcp会分包。
一张图片可能2M甚至更大,当然普通的验证码截图很小400k左右。但考虑到健壮性、通用性,还是应当通过设计来解决这个问题。
2)文件长度不对可能导致损坏
一张图片如果头部少了或者多了一些信息是很有可能导致图片无法打开,更无从说识别。而尾部多了少了相对不那么敏感,但最好不要弄错。试想,如果客户的照片效果很好,由于你在传递过程中多接收或者少接收数据导致打了折扣,加班说不定又要开始了。所以,还是精确为好。
而且如果一个文件是1800字节,一包tcp一般1500字节以内,你rcv一次直接存文件,文件够呛能打开。而如果是300字节的文件,你直接写进去1400字节,肯定有问题。
所以,文件长度是文件传输的一个必须前知的参数。
针对这两个问题,用协议来解决。此处自定义了一个很简单的协议如下:
filename seperator filelen seperator filecontent
也就是将传输的内容分成3段,第一段是文件名,第二段是文件长度,第三段是文件内容。都是以字节流的形式封装传递。
各段用分隔符加以分隔,此处为一个tab。
2、server端代码
刚过完年,笔记本没装pycharm,代码纯用vim写的,所以注释空格很多未符合pep8规则的地方,大家自行忽略。。。vim的插件配置还在摸索。。。
#encoding:utf-8
#!/usr/bin/env python
#导入socket模块
import socket
import sys
import pytesseract
from PIL import Image
#添加异常处理,添加tcp断链处理与保护判断
def getCode(path):
"""
get identify code with pytesseract
"""
img=Image.open(path)
vcode=pytesseract.image_to_string(img)
print(vcode)
return vcode
class Proto:
def init(self):
print("hehe")
def decode(self,cont):
print("decode in parent")
#从第一包数据解析出文件名、长度、文件内容起始处
class HeadAnalyzor(Proto):
def init(self):
contPos=0
contLen=0
name=""
def decode(self,cont):
print(cont)
nameEnd=cont.find(bytes(' ','utf8'))
self.name=cont[0:nameEnd]
self.contPos=cont.find(bytes(' ','utf8'),nameEnd+1)
print("na %d co %d" %(nameEnd,self.contPos))
self.contLen=(int)(cont[nameEnd:self.contPos])
print("nameEnd %d contPos %d contLen %d" %(nameEnd,self.contLen,self.contLen))
#将文件内容存入文件中
class FileTransferProto(Proto):
def decode(self,cont):
print(type(cont))
pos=cont.find(bytes(' ','utf8'))
name=cont[0:pos]
pos=cont.find(bytes(' ','utf8'),pos+1)
print(" pos is %d" %(pos))
if pos>0:
f=open(name,'wb')
print("fName is %s len is %d" %(name,len(cont)-pos))
f.write(cont[pos+1:-1])
f.close()
else:
print("not found space ")
#开启ip和端口
ip_port = ('192.168.65.49',(int)(sys.argv[1]))
#生成句柄
web = socket.socket()
#绑定端口
web.bind(ip_port)
#最多连接数
web.listen(5)
#等待信息
print ('nginx waiting...')
#阻塞
conn,addr = web.accept()
print("accepted")
#开启死循环
while True:
print("again")
#获取客户端请求数据
data = conn.recv(1024000)
buf = data
#客户端断开链接
if len(data)==0:
print("disconnect by peer! rcvLen:%d \n waiting..." %len(data))
#重连
conn,addr=web.accept()
continue
headAna = HeadAnalyzor()
headAna.decode(data)
remainLen=headAna.contLen -len(data) + headAna.contPos + 1
rcvLen=len(data)
i=0
#直到指定长度的文件内容均接收到后进行存文件操作
while remainLen>0:
data=conn.recv(1024)
rcvLen+=len(data)
remainLen-=len(data)
print("times %d remain %d conLen %d rcvLen %d" %(i,remainLen,headAna.contLen,rcvLen))
i+=1
buf+=data
proto=FileTransferProto()
proto.decode(buf)
#向对方发送数据,也即识别出来的验证码
idcode=getCode(headAna.name)
print(conn.send(bytes(idcode,'utf8')))
#关闭链接
# conn.close()
运行脚本:
python3 tcpserver.py 8889
3、client端代码
基于Qt写的,此处摘写代码片段。
1)、读文件封装数据
int QEasyWrapper::wrap(char *buf, std::size_t maxlen)
{
memset(buf,0,maxlen);
QFile f(fPath_.c_str());
f.open(QIODevice::ReadOnly);
QByteArray ba=f.readAll();
std::cout<<ba.size()<<"cont:"<<std::endl;
f.close();
if(0>= ba.size()){
std::cout<<"file open err"<<std::endl;
return -1;
}
sprintf(buf,"%s%c%d%c",fName_.c_str(),sep_,ba.size(),sep_);
int headLen=strlen(buf);
int cpyLen=std::min<int>(maxlen,ba.length()+headLen);
memcpy(&buf[headLen],ba.data(),cpyLen);
std::cout<<"headLen"<<headLen<<";wholeLen:"<<cpyLen<<std::endl;
return cpyLen;
}
2)、利用QTcpSocket进行数据的传输与接收
发送:
int TcpFileSender::write(char *buf, std::size_t len)
{
int sndLen=sock->write(buf,len);
std::cout<<"waitforwrite:"<<sock->waitForBytesWritten(5000)<<std::endl;
std::cout<<"len:"<<len<<";sndLen:"<<sndLen<<std::endl;
return sndLen;
}
接收:
int TcpFileSender::read(char *buf, std::size_t buflen)
{
std::cout<<"waitforread:"<<sock->waitForReadyRead(5000)<<std::endl;
if(rcvBuf_.size()<1 || buflen<1)
return 0;
QByteArray ba=sock->readAll();
std::cout<<"rcv:"<<ba.size()<<";"<<ba.data()<<std::endl;
int cpyLen=std::min<std::size_t>(buflen,ba.size());
memcpy(buf,ba.data(),cpyLen);
return cpyLen;
}
tips
QtcpSocket在使用过程中 发送后与接收前一定要成套调用waitfor相关的函数。
如果不调用,发送之后立即调用接收的读相关函数,接收到的将是空,因为write和read 这2个接口是非阻塞的,直接执行,发还没发完呢就开始读了,当然收不到数据。当然,用connect将ReadyRead信号绑定到一个函数进行接收是可以收到数的。
还有一种情况,write之后未加waitfor,read之前加了waitfor,会报超时。因为你发的时候直接就返回,未等待其执行完毕,然后调用waitForReadyRead,直接将socket阻塞掉了,相当于发了一半被waitForReadyRead阻塞了,服务端未收齐数据自然不会回复。这样最终就超时了。
针对这个细节,个人理解,QTcpSocket底层做了一个“线程”封装进行数据的收发,waitFor相关函数会阻塞此线程。而且直接在主线程中调用Sleep也是不行的。此“线程”还非彼线程,会随主线程一起被阻塞。待有机会再研究下。欢迎熟悉底层或者源码的同学拍砖指正。
:)
本文永久更新链接地址:http://www.linuxidc.com/Linux/2017-03/142018.htm