• 网络编程之socket


    1.socket概念

      也叫做套接字。用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求,它是一个处于应用层和网路层之间的一个封装起来供人使用的接口

      面向连接(TCP):通信之前一定要建立一条连接,这种通信方式也被成为”虚电路“或”流套接字“。面向连接的通信方式提供了顺序的、可靠地、不会重复的数据传输,而且也不会被加上数据边界。这意味着,每发送一份信息,可能会被拆分成多份,每份都会不多不少地正确到达目的地,然后重新按顺序拼装起来,传给正等待的应用程序。

      要创建TCP套接字就得创建时指定套接字类型为SOCK_STREAM(默认不写)  

      无连接(UDP):无需建立连接就可以通讯。但此时,数据到达的顺序、可靠性及不重复性就无法保障了。数据报会保留数据边界,这就表示数据是整个发送的,不会像面向连接的协议先拆分成小块。它就相当于邮政服务一样,邮件和包裹不一定按照发送顺序达到,有的甚至可能根本到达不到。而且网络中的报文可能会重复发送。  

      要创建UDP套接字就得创建时指定套接字类型为SOCK_DGRAM

    2.用法

      first:实例化对象,构造函数:

        obj = socket(family, type, proto, fileno, fileno)

        family:地址簇,表示使用的协议,即TCP/IPv4的协议

        type:表示TCP​或者UDP

        protocol :协议号,默认为0,一般不填

        fileno:如果指定了fileno,其他参数将被忽略

      second:进行绑定:

        socket.bind(address)​

    ​    address:IP地址与端口,这里是一个元组

      third:开始监听:

        socket.listen()

        之前版本listen要添加参数n,参数n表示同一时间可以有n个链接等待与server端通信,一般不用

      fourth:调用套接字函数等待连接

        connection,address = socket.accept()

        调用accept方法时,socket会进入'waiting'(或阻塞)状态。客户请求连接时,方法建立连接并返回服务器。accept方法返回一个含有俩个元素的元组,形如(connection,address)。第一个元素(connection)是新的socket对象,服务器通过它与客户通信;第二个元素(address)是客户的internet地址。

      fifth:处理数据

        服务器和客户通过send和recv方法传输数据。

        服务器调用send,采用字符串形式向客户发送信息,send方法返回已发送的字符个数。

        服务器使用recv方法从客户接受信息。

        调用recv时,必须指定一个整数来控制本次调用所接受的最大数据量。recv方法在接受数据时会进入'blocket'状态,最后返回一个字符串,用它来表示收到的数据。如果发送的量超过recv所允许,数据会被截断。多余的数据将缓冲于接收端。以后调用recv时,多余的数据会从缓冲区删除。

      sixth:关闭套接字

    用法案例:

    基于TCP协议的socket(TCP是基于链接的,必须先启动服务端,自启动客户端去链接客户端)

    server端:

    import socket
    soc = socket.socket()            # 创建一个soc对象
    soc.bind(('127.0.0.1',9789))     # 将地址绑定到套接字
    soc.listen()                     # 监听端口
    conn,addr = soc.accept()         # 接收客户端口链接
    re = conn.recv(1024)             # 接收客户端信息
    print(re.decode('utf-8'))      # 接收客户端发送的信息,必须转码成unicode
    conn.send('海贼王'.encode('utf-8'))  # 给客户端发送信息,必须是bytes类型
    conn.close()
    soc.close()

    client端:

    import socket
    me = socket.socket()             # 实例化对象
    me.connect(('127.0.0.1',9789))   # 把地址绑定到套接字
    me.send('one piece'.encode('utf-8'))  # 给server发送消息
    ret = me.recv(1024)              # 设置固定接收字节,防止一次加载过多占内存
    print(ret.decode('utf-8'))         # 将server端发来的信息解码
    me.close()                         # 关闭端口

    易错点:这里客户端和服务端发送消息时,必须注意接收与发送顺序

     计算机回环地址:

      127.0.0.1,默认为本机地址,一般在自己电脑测试使用,它不用再过交换机查询

     tcp协议适用范围:

      适用于文件的上传和下载,以及发送重要文件等。每和一个客户端建立连接,都会在自己的操作系统上占用一个资源

      它同一个时间段只能和一个客户端建立连接

    基于UDP协议的socket(UDP是无链接的,先启动哪一端都不会报错)

     用法案例:

    server端:

    import socket
    ser = socket.socket(type=socket.SOCK_DGRAM)
    ser.bind(('127.0.0.1', 8888))
    mes,addr = ser.recvfrom(1024)
    print(mes.decode('utf-8'))
    ser.sendto('海贼王'.encode('utf-8'), addr)
    ser.close()
    

    client端:

    import socket
    client = socket.socket(type=socket.SOCK_DGRAM)
    addr = ('127.0.0.1', 8888)
    client.sendto('onepiece'.encode('utf-8'),addr)
    mess,addr = client.recvfrom(1024)
    print(mess.decode('utf-8'))
    client.close()
    

      这样写虽然没有什么问题,但是基于需要来回编码以及解码,显得很不pythonic,为了解决这个问题,我们特别为内置的socket类编写一个子类,来解决编码转换的问题

    创建类方法:

    from socket import *
    class Mysocket(socket):
    	def __init__(self,coding='utf-8'):
    		self.coding = coding
    		super().__init__(type=SOCK_DGRAM)
    
    	def mysend(self,mess,addr):
    		return self.sendto(mess.encode(self.coding),addr)
    
    	def myrecv(self,num):
    		mess,addr = self.recvfrom(num)
    		return mess.decode(self.coding),addr  

      创建客户端和接收端

    # server端
    from my_test import Mysocket  # 从文件路径中导入这个类
    ser = Mysocket()
    ser.bind(('127.0.0.1', 8888))
    mes,addr = ser.myrecv(1024)
    print(mes)
    ser.mysend('海贼王', addr)
    ser.close()
    
    
    # 用户端
    from my_test import Mysocket
    client = Mysocket()
    addr = ('127.0.0.1', 8888)
    client.mysend('onepiece',addr)
    mess,addr = client.myrecv(1024)
    print(mess)
    client.close() 

      在UDP下基于服务端完成客户端的时间同步服务,比如机房中的所有机器每隔一段时间都会请求服务器,来获取一个标准时间

    # server端

    import time
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',9200)
    sk.bind(ip_port)
    while True:
        msg,addr = sk.recvfrom(1024)   # 接收的是用户端格式化字符串字节码
        sk.sendto(time.strftime(msg.decode('utf-8')).encode('utf-8'),addr)
    sk.close()
    

    # client端

    import socket
    import time
    tb = socket.socket(type=socket.SOCK_DGRAM)
    ip_port = ('127.0.0.1',9200)
    while True:
        tb.sendto('%Y/%m/%d %H:%M:%S'.encode('utf-8'),ip_port)
        mes,addr = tb.recvfrom(1024)
        print(mes.decode('utf-8'))
        time.sleep(1)
    tb.close()
    

    3.关于TCP下的黏包问题

    # server端
    import socket
    ser = socket.socket()
    ip_port = ('127.0.0.1',8000)
    ser.bind(ip_port)
    ser.listen()
    myser,addr = ser.accept()
    myse = myser.recv(1024)
    print(myse.decode('utf-8'))
    myser.close()
    ser.close()
    
    
    # client端
    import socket
    client = socket.socket()
    ip_port=('127.0.0.1',8000)
    client.connect(ip_port)
    client.send('第一次'.encode('utf-8'))
    client.send('第二次'.encode('utf-8'))
    client.send('第三次'.encode('utf-8'))
    client.send('第四次'.encode('utf-8'))
    client.send('第五次'.encode('utf-8'))
    client.close()
    
    server端接收结果:第一次第二次第三次第四次第五次
    

     再看看这个:

    # server端
    import socket
    ser = socket.socket()
    ip_port = ('127.0.0.1',8000)
    ser.bind(ip_port)
    ser.listen()
    myser,addr = ser.accept()
    myser.send('一次一次一次'.encode('utf-8'))
    myser.send('二次'.encode('utf-8'))
    myser.send('三次'.encode('utf-8'))
    myser.send('四次'.encode('utf-8'))
    myser.send('五次'.encode('utf-8'))
    myser.close()
    ser.close()
    
    # client端
    import socket
    client = socket.socket()
    ip_port=('127.0.0.1',8000)
    client.connect(ip_port)
    s = client.recv(9)   # 一次没有完整接收
    print(s.decode('utf-8'))
    s1 = client.recv(9)  # 这次接收会继续接收上次剩下的数据流
    print(s1.decode('utf-8'))
    s2 = client.recv(9)
    print(s2.decode('utf-8'))
    s3 = client.recv(9)
    print(s3.decode('utf-8'))
    s4 = client.recv(9)
    print(s4.decode('utf-8'))
    s5 = client.recv(3)
    print(s5.decode('utf-8'))
    client.close()
    
    # client接收:
    一次一
    次一次
    二次三
    次四次
    五次

    为什么client端的消息在server端可以一次性全部接收呢?第二个例子中server端的信息需要等待缓冲区满才发送过去呢?

      TCP协议是面向流(stream)的协议,发送端为了将多个发往接收端的包更有效的发送给对方,使用了(Nagle算法)优化方法,将多次间隔较小且数据量小的数据合并成一个大数据块,然后进行。这样接收端就难以分辨出来了

      所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

      tcp是基于数据流的,所以收发消息不能为空,这就需要在两个端口进行空消息处理机制,防止程序卡住

      udp是基于数据报的,消息可为空(因为它存在消息保护边界)

      UDP是面向消息的协议,每个UDP段都是一个消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

    特点:

      udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

      tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    4.解决黏包的办法(导用struct模块)

      黏包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

     这里利用struct模块可以把一个类型(如数字),转换成固定长度的bytes类型(四个字节

    import struct
    s = struct.pack('i',12346587)
    print(s)    # b'xdbdxbcx00'
    a = struct.unpack('i',b'xdbdxbcx00')
    print(a)   # (12346587,)  

      用法:

     server端

    import os
    import json
    import struct
    import socket
    ser = socket.socket()
    ip_port = ('127.0.0.1',8000)
    ser.bind(ip_port)
    ser.listen()
    conn,addr = ser.accept()
    
    filename = '塑料王国.mp4'
    filesize = os.path.getsize('E:电影塑料王国.mp4')
    dic = {'filename':filename,'filesize':filesize}
    str_dic = json.dumps(dic)
    len_dic = struct.pack('i',len(str_dic))
    conn.send(len_dic)    # 这里必须是先发送字符串长度
    conn.send(str_dic.encode('utf-8'))
    
    with open('E:电影塑料王国.mp4','rb') as f:
    	while True:
    		block = f.read(2048)
    		conn.send(block)
    		if not block:
    			break
    ser.close()
    conn.close()
    

     client端

    import socket
    import json
    import struct
    cli = socket.socket()
    ip_port = ('127.0.0.1',8000)
    cli.connect(ip_port)
    
    sizedic = cli.recv(4)    # 这里固定接收4个字节,和struct用法一致
    sizedic = struct.unpack('i',sizedic)[0]
    str_size = cli.recv(sizedic).decode('utf-8')
    strdid = json.loads(str_size)
    print(strdid)
    with open('电影.mkv','wb') as f:
    	while True:
    		conn = cli.recv(2048)  # 两边可以不用一样,这里接收任意大小
    		f.write(conn)
    		if not conn:
    			break
    
    cli.close()

    5.验证客户端合法性 

     这里首先使用了os模块中的os.urandom(32)方法生成了一个固定长度且可变的bytes字节码

     另MAC(Message Authentication Code,消息认证码算法)是含有密钥的散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加入了密钥。

     server端

    import os
    import socket
    import hmac
    serect_key = '这是一个秘钥'.encode('utf-8')
    sk=socket.socket()
    sk.bind(('127.0.0.1',9000))
    sk.listen()
    while True:
    	try:
    		conn,addr = sk.accept()
    		rand = os.urandom(32)  # 生成32位随机bytes字节码
    		conn.send(rand)
    		obj = hmac.new(key=serect_key,msg=rand)
    		ret = obj.hexdigest()  # 得到一个密文
    		msg = conn.recv(1024).decode('utf-8')
    		if msg == ret:print('合法用户')
    		else:conn.close()
    	finally:
    		sk.close()
    		break
    

     client端

    import socket
    import hmac
    serect_key = '这是一个秘钥'.encode('utf-8')
    client = socket.socket()
    client.connect(('127.0.0.1',9000))
    urandom = client.recv(1024)     # 客户端接收32位随机bytes字节码
    hmac_obj = hmac.new(key=serect_key,msg=urandom)
    client.send(hmac_obj.hexdigest().encode('utf-8'))
    client.close() 

     6.socketserver的使用(起到了并发编程的作用)

       通过该模块实现了多个client端可以同一时间向server端发送消息(它的内部其实通过并发编程实现的)

     server端-----固定格式

    import socketserver
    class Myserver(socketserver.BaseRequestHandler):
    	def handle(self):   # 创建一个方法,这里规定方法名必须叫做handle
    		self.request.send('server端消息'.encode('utf-8'))
    		msg = self.request.recv(1024).decode('utf-8')
    		print(msg)
    		# 这里的self.request就是conn,相当于拿到了链接
    if __name__ == '__main__':
    	socketserver.TCPServer.allow_reuse_address = True
    			# 这话设置为True,防止端口报错
    	server = socketserver.ThreadingTCPServer(('127.0.0.1',8000),Myserver)
    	server.serve_forever()
    

     client端

    import socket
    cli = socket.socket()
    cli.connect(('127.0.0.1',8000))
    ret = cli.recv(1024).decode('utf-8')
    print(ret)
    inp = input('>>>').encode('utf-8')
    cli.send(inp)
    cli.close()
    

    7.websocket的概念及使用  

     猛戳链接

  • 相关阅读:
    多线程
    Flume和 Sqoop
    Struts2部分
    hibernate学习笔记(一)
    idea创建普通的java小项目教程
    IntelliJ Idea 常用快捷键列表
    idea创建springMVC框架和配置小文件
    对于Git的总结
    关于jvm运行时时区的总结
    事务的总结笔记(详解很仔细),mysql事务隔离级别演示,,,
  • 原文地址:https://www.cnblogs.com/LearningOnline/p/8986086.html
Copyright © 2020-2023  润新知