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_INET6
。SOCK_STREAM
指定使用面向流的TCP协议,这样,一个Socket
对象就创建成功,但是还没有建立连接。
注意参数是一个tuple
,包含地址和端口号。
建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
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'\r\n\r\n', 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()
客户端:
#买手机 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()
效果:
这里会用到新的2个模块。
struct: http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
subprocess: http://blog.csdn.net/imzoer/article/details/8678029