网络编程之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'\xdbd\xbc\x00'
a = struct.unpack('i',b'\xdbd\xbc\x00')
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的概念及使用  

 猛戳链接

 

posted @ 2018-05-05 11:03  -Learning-  阅读(407)  评论(0编辑  收藏  举报