• socket网络编程


    TCP编程:

    Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

    大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

    先来看看客户端怎么创建一个基于TCP连接的Socket:

    # 导入socket库:
    import socket
    
    # 创建一个socket:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 建立连接:
    s.connect(('www.sina.com.cn', 80))  #Web服务的标准端口都是80,所以这里设置80端口

    创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。

    注意参数是一个tuple,包含地址和端口号。

    建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

    # 发送数据:
    s.send(b'GET / HTTP/1.1
    Host: www.sina.com.cn
    Connection: close
    
    ')
    

    TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。

    发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

    # 接收数据:
    buffer = []
    while True:
        # 每次最多接收1k字节:
        d = s.recv(1024)
        if d:
            buffer.append(d)
        else:
            break
    data = b''.join(buffer)

    接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

    当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

    # 关闭连接:
    s.close()
    

    接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

    header, html = data.split(b'
    
    ', 1)
    print(header.decode('utf-8'))
    # 把接收的数据写入文件:
    with open('sina.html', 'wb') as f:
        f.write(html)

    现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

    参考,廖雪峰Python:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001432004374523e495f640612f4b08975398796939ec3c000

    服务端套接字函数
    s.bind() 绑定(主机,端口号)到套接字
    s.listen() 开始TCP监听
    s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

    客户端套接字函数
    s.connect() 主动初始化TCP服务器连接
    s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

    公共用途的套接字函数
    s.recv() 接收TCP数据
    s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    s.recvfrom() 接收UDP数据
    s.sendto() 发送UDP数据
    s.getpeername() 连接到当前套接字的远端的地址
    s.getsockname() 当前套接字的地址
    s.getsockopt() 返回指定套接字的参数
    s.setsockopt() 设置指定套接字的参数
    s.close() 关闭套接字

    面向锁的套接字方法
    s.setblocking() 设置套接字的阻塞与非阻塞模式
    s.settimeout() 设置阻塞套接字操作的超时时间
    s.gettimeout() 得到阻塞套接字操作的超时时间

    面向文件的套接字的函数
    s.fileno() 套接字的文件描述符
    s.makefile() 创建一个与该套接字相关的文件

    粘包:
    当我们基于socket传输数据时,可能会发生粘包。粘包问题主要是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

    两种情况下会发生粘包。

    
    

    发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

    接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

    下面将结合客户端(windows)访问服务端(Linux)达成类似ssh协议的效果,在客户端上输入指令,访问服务端并执行指令。并且解决粘包的问题,上代码。
    服务端:
    #coding:utf-8
    #买手机
    import socket   #导入必备的模块
    import struct       #打包与解包
    import json         #格式化,类似eval
    import subprocess    #模块用于启动一个新的进程并且与之通信
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    #创建一个socket,AF_INET指定使用IPv4协议,如果要用IPv6,就指定为AF_INET6
    #SOCK_STREAM指定使用面向流的TCP协议
    #绑定电话卡
    ip_port = ('192.168.20.128',8081)  #建立连接,大于1024的端口可以任意使用
    phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #加入一条socket配置,重用ip和端口,防止四次挥手time_wait状态在占用地址
    phone.bind(ip_port)  #监听端口
    #开机
    phone.listen(5)    #调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量
    #等待电话
    
    #链接循环
    while True:  #服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:
        conn,addr = phone.accept()      #接受一个新连接
        print('client addr',addr)  #验证已经连接上
        #通讯循环
        while True:
            try:
                cmd = conn.recv(1024)  #每次最多接收1K字节
                res = subprocess.Popen(cmd.decode('utf-8'),  #使用Popen来创建进程,并与进程进行复杂的交互
                                       shell=True, #表示程序由shell执行
                                       stdout=subprocess.PIPE, #输出
                                       stderr=subprocess.PIPE)  #错误输出 PIPE表示管道接收
                out_res = res.stdout.read()  #定义一个接收管道输出的变量
                err_res = res.stderr.read()  #定义一个接收错误输出的变量
                data_size = len(out_res)+len(err_res)  #获取发送的数据所占大小
                head_dic = {'data_size':data_size}      #数据的大小可以有无限大,所以很难定义struct的方法,可以是'i'可以是'q',所以这里将数据大小以字典的形式封装,然后将字典作为报头发送
                head_json = json.dumps(head_dic)     #将python字典类型解析为json数据
                head_bytes = head_json.encode("utf-8")      #将数据以utf8编码
    
                #part1:先发报头的长度
                head_len = len(head_bytes)          #获取报头长度
                conn.send(struct.pack('i',head_len))        #字典长度struct方式打包并发送到客户端
                #part2:再发送报头
                conn.send(head_bytes)           #此时客户端已经知道报头长度,发送报头不会产生粘包
                #part3:最后发送数据
                conn.send(out_res)
                conn.send(err_res)
    
            except Exception:  #如果客户端挂断通讯服务端会抛出异常,这里加入万能异常处理让服务端在客户端挂断后能正常运行
                break
    
        conn.close()
    phone.close()
    View Code

    客户端:

    #买手机
    import socket
    import struct
    import json
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    #拨通电话
    ip_port = ('192.168.20.128',8081)           #连接服务端的IP与端口
    phone.connect(ip_port)
    
    #通信循环
    while True:
        # 发消息
        cmd = input('>>>:').strip()
        if not cmd: continue        #如果发空,则继续输入
        phone.send(bytes(cmd,encoding='utf8'))      #客户端发送指令
    
        #part1:先收报头的长度
        head_struct = phone.recv(4)         #我们自定了服务端发送报头的struct方式是'i'格式,所以这里接收4个字节则是完整的报头
        head_len = struct.unpack('i',head_struct)[0]        #解包,返回一个元组,第一个元素即是报头的长度,即字典长度
    
        #part2:再收报头
        head_bytes = phone.recv(head_len)       #规定接收到报头长度的数据,即接收到了完整的字典
        head_json = head_bytes.decode('utf-8')  #收到的数据进行解码并交给变量
    
        head_dic = json.loads(head_json)    #同样的对dumps了的数据用loads方法解析为Python格式的数据
        print(head_dic)
        data_size = head_dic['data_size']      #拿到了字典,再取出对应key里的真正需要的数据的长度值
    
        #part3:收数据
        recv_size = 0       #先定义一个变量,比对接收的数据
        recv_data = b''     #初始化一个空bytes
        while recv_size<data_size:      #比对数据大小,循环接收数据直到接收到的数据大小等于服务端返回的数据大小
            data = phone.recv(1024)     #每次接收1k字节
            recv_size+=len(data)    #接收总数据循环加收到的实际数据来比对
            recv_data+=data         #将收到的数据拼接
    
        print(recv_data.decode('utf-8'))    #解码数据并打印
    phone.close()
    View Code

    效果:

    这里会用到新的2个模块。

    struct:     http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html

    subprocess:    http://blog.csdn.net/imzoer/article/details/8678029

     
  • 相关阅读:
    .net反编译原理
    科学使用Log4View2
    头条一面竟然问我Maven?
    SpringCloud Netflix(一) :微服务架构
    Linux环境安装Docker
    Quartz定时任务
    Jedis连接外部Redis
    宝塔phpmyadmin打不开的可能问题及解决方法
    文件上传 Window & Linux
    SpringBoot登录判断
  • 原文地址:https://www.cnblogs.com/mitsui/p/6802524.html
Copyright © 2020-2023  润新知