首页 > 代码库 > PythonDay8
PythonDay8
本章知识点:
1.Socket语法及相关
2.Socket单线程
3.Socket多线程
4.分分钟教你做个FTP
什么是socket?
Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
Socket 方法
socket.
socket
(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
Create a new socket using the given address family, socket type and protocol number. The address family should be AF_INET
(the default), AF_INET6
, AF_UNIX
, AF_CAN
or AF_RDS
. The socket type should beSOCK_STREAM
(the default), SOCK_DGRAM
, SOCK_RAW
or perhaps one of the other SOCK_
constants. The protocol number is usually zero and may be omitted or in the case where the address family is AF_CAN
the protocol should be one of CAN_RAW
or CAN_BCM
. If fileno is specified, the other arguments are ignored, causing the socket with the specified file descriptor to return. Unlike socket.fromfd()
, fileno will return the same socket and not a duplicate. This may help close a detached socket using socket.close()
.
socket.
socketpair
([family[, type[, proto]]])
Build a pair of connected socket objects using the given address family, socket type, and protocol number. Address family, socket type, and protocol number are as for the socket()
function above. The default family is AF_UNIX
if defined on the platform; otherwise, the default is AF_INET
.
socket.
create_connection
(address[, timeout[, source_address]])
Connect to a TCP service listening on the Internet address (a 2-tuple (host, port)
), and return the socket object. This is a higher-level function than socket.connect()
: if host is a non-numeric hostname, it will try to resolve it for both AF_INET
and AF_INET6
, and then try to connect to all possible addresses in turn until a connection succeeds. This makes it easy to write clients that are compatible to both IPv4 and IPv6.
Passing the optional timeout parameter will set the timeout on the socket instance before attempting to connect. If no timeout is supplied, the global default timeout setting returned by getdefaulttimeout()
is used.
If supplied, source_address must be a 2-tuple (host, port)
for the socket to bind to as its source address before connecting. If host or port are ‘’ or 0 respectively the OS default behavior will be used.
socket.
getaddrinfo
(host, port, family=0, type=0, proto=0, flags=0) #获取要连接的对端主机地址
sk.bind(address)
s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
这个值不能无限大,因为要在内核中维护连接队列
sk.setblocking(bool)
是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
接收TCP 客户的连接(阻塞式)等待连接的到来
sk.connect(address)
连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
sk.connect_ex(address)
同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
sk.close()
关闭套接字
sk.recv(bufsize[,flag])
接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
sk.recvfrom(bufsize[.flag])
与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag])
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
sk.sendall(string[,flag])
将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
内部通过递归调用send,将所有内容发送出去。
sk.sendto(string[,flag],address)
将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
sk.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
sk.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)
sk.fileno()
套接字的文件描述符
socket.
sendfile
(file, offset=0, count=None)
发送文件 ,但目前多数情况下并无什么卵用。
客户端
大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来的,后面的通信就是发送网页内容了。
服务器
和客户端编程相比,服务器编程就要复杂一些。
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
OK,那我们先来写一个入门简单的一个socket:
server:
1 import socket 2 server=socket.socket()#定义个socket实例 3 server.bind(("localhost",9999))#绑定端口 4 server.listen()#监听端口 5 print("开始监听") 6 conn,addr=server.accept()#conn代表传入类型,addr代表地址 接收连接并返回 7 print("新的连接",addr) 8 data=http://www.mamicode.com/conn.recv(1024)#接收客户端发送的字节 9 print("收到消息",data)10 server.close()
client:
1 import socket2 client=socket.socket()#定义socket类型3 client.connect(("localhost",9999))#连接服务器地址4 client.send(b‘hello‘)#发送消息5 client.close()
OK,这就是一个简单的一socket,那么问题来了,我这个服务在收到客户端一个消息后就断开了,那么怎么让他不断开呢,怎么让他一直持续呢?
多次数据交互怎么实现呢?
server:
import socketserver=socket.socket()#定义个socket实例server.bind(("localhost",9999))#绑定端口server.listen()#监听端口print("开始监听")conn,addr=server.accept()#conn代表传入类型,addr代表地址 接收连接并返回print("新的连接",addr)while True: data=conn.recv(1024)#接收客户端发送的字节 print("收到消息",data) conn.send(data.upper())#将收到的消息返回客户端server.close()
client:
import socketclient=socket.socket()#定义socket类型client.connect(("localhost",9999))#连接服务器地址while True: date=input("-->:").strip() client.send(date.encode("utf-8"))#发送消息 data=http://www.mamicode.com/client.recv(1024)#接收服务端消息 print("来自服务器",data)client.close()
实现了多次交互, 棒棒的, 但你会发现一个小问题, 就是客户端输入为空,就会卡着不动,为啥呢?
因为服务器默认是不接受为空的。
ok知道了原因,那我们来解决下:
1 import socket 2 client=socket.socket()#定义socket类型 3 client.connect(("localhost",9999))#连接服务器地址 4 while True: 5 date=input("-->:").strip() 6 if len(date)==0:continue;#做个判断,为空直接跳出当前循环 7 client.send(date.encode("utf-8"))#发送消息 8 data=http://www.mamicode.com/client.recv(1024)#接收服务端消息 9 print("来自服务器",data)10 client.close()
那么咱们到目前为止单线程的小程序就完成了,但是,如何将用户作为多线程进行操作呢?药不能停,对吧
比如,你正在通话中,你一个电话进来了,那你是不是可以挂断当前去接听另一个电话是吧,ok这就是多线程
socket多线程
ok,那我们先来回顾下这段代码:
conn,addr=server.accept()#
这段代码代表的是接入一个新的类型,那么我们可不可以给他做个循环,让他结束当前循环后,继续下一个循环,这样,多线程是不是就有头绪了?
通过socket实现简单的socket功能
那么,光是简单的收发消息就没意思了,那我们来做个ssh吧。
server:
1 import socket 2 import os 3 server=socket.socket()#获取socket实例 4 server.bind(("localhost",8888))#绑定端口 5 server.listen()#监听端口 6 while True:#第一层 7 print("等待客户端连接") 8 conn,addr=server.accept()#接收客户端请求,程序在这里开始阻塞,等待客户连接进来 9 print("新连接",addr)10 while True:11 data=http://www.mamicode.com/conn.recv(1024)#接收数据大小12 if not data:13 print("客户端断开")14 break15 print("收到指令",data)16 res=os.popen(data.decode()).read()#py3 里socket发送的只有bytes,os.popen又只能接受str,所以要decode一下17 print(len(res))18 conn.send(res.encode("utf-8"))#返回客户端信息19 server.close()
client:
1 import socket 2 import os 3 client=socket.socket() 4 client.connect(("localhost",8888)) 5 while True: 6 msg=input("-->:") 7 if len(msg)==0:continue 8 client.send(msg.encode("utf-8")) 9 data=http://www.mamicode.com/client.recv(1024)#接收客户端返回的消息10 print(data.decode())11 client.close()
very cool , 这样我们就做了一个简单的ssh , 但多试几条命令你就会发现,上面的程序有以下2个问题。
- 不能执行top等类似的 会持续输出的命令,这是因为,服务器端在收到客户端指令后,会一次性通过os.popen执行,并得到结果后返回给客户,但top这样的命令用os.popen执行你会发现永远都不会结束,所以客户端也永远拿不到返回。(真正的ssh是通过select 异步等模块实现的,我们以后会涉及)
- 不能执行像cd这种没有返回的指令, 因为客户端每发送一条指令,就会通过client.recv(1024)等待接收服务器端的返回结果,但是cd命令没有结果 ,服务器端调用conn.send(data)时是不会发送数据给客户端的。 所以客户端就会一直等着,等到天荒地老,结果就卡死了。解决的办法是,在服务器端判断命令的执行返回结果的长度,如果结果为空,就自己加个结果返回给客户端,如写上"cmd exec success, has no output."
- 如果执行的命令返回结果的数据量比较大,会发现,结果返回不全,在客户端上再执行一条命令,结果返回的还是上一条命令的后半段的执行结果,这是为什么呢?这是因为,我们的客户写client.recv(1024), 即客户端一次最多只接收1024个字节,如果服务器端返回的数据是2000字节,那有至少9百多字节是客户端第一次接收不了的,那怎么办呢,服务器端此时不能把数据直接扔了呀,so它会暂时存在服务器的io发送缓冲区里,等客户端下次再接收数据的时候再发送给客户端。 这就是为什么客户端执行第2条命令时,却接收到了第一条命令的结果的原因。 这时有同学说了, 那我直接在客户端把client.recv(1024)改大一点不就好了么, 改成一次接收个100mb,哈哈,这是不行的,因为socket每次接收和发送都有最大数据量限制的,毕竟网络带宽也是有限的呀,不能一次发太多,发送的数据最大量的限制 就是缓冲区能缓存的数据的最大量,这个缓冲区的最大值在不同的系统上是不一样的, 我实在查不到一个具体的数字,但测试的结果是,在linux上最大一次可接收10mb左右的数据,不过官方的建议是不超过8k,也就是8192,并且数据要可以被2整除,不要问为什么 。anyway , 如果一次只能接收最多不超过8192的数据 ,那服务端返回的数据超过了这个数字怎么办呢?比如让服务器端打开一个5mb的文件并返回,客户端怎么才能完整的接受到呢?那就只能循环收取啦。
在开始解决上面问题3之前,我们要考虑,客户端要循环接收服务器端的大量数据返回直到一条命令的结果全部返回为止, 但问题是客户端知道服务器端返回的数据有多大么?答案是不知道,那既然不知道服务器的要返回多大的数据,那客户端怎么知道要循环接收多少次呢?答案是不知道,擦,那咋办? 总不能靠猜吧?呵呵。。。 当然不能,那只能让服务器在发送数据之前主动告诉客户端,要发送多少数据给客户端,然后再开始发送数据,yes, 机智如我,搞起。
server:
1 import socket 2 import os 3 server=socket.socket()#获取socket实例 4 server.bind(("localhost",8888))#绑定端口 5 server.listen()#监听端口 6 while True:#第一层 7 print("等待客户端连接") 8 conn,addr=server.accept()#接收客户端请求,程序在这里开始阻塞,等待客户连接进来 9 print("新连接",addr)10 while True:11 data=http://www.mamicode.com/conn.recv(1024)#接收数据大小12 if not data:13 print("客户端断开")14 break15 print("收到指令",data)16 res=os.popen(data.decode()).read()#py3 里socket发送的只有bytes,os.popen又只能接受str,所以要decode一下17 print(len(res))18 conn.send(str(len(res.encode())).encode("utf-8"))#先发字节19 conn.send(res.encode("utf-8"))#再发数据20 print("send done")21 server.close()
client:
1 import socket 2 import os 3 client=socket.socket() 4 client.connect(("localhost",8888)) 5 while True: 6 msg=input("-->:") 7 if len(msg)==0:continue 8 client.send(msg.encode("utf-8")) 9 cmd_res_size=client.recv(1024)#接收结果的长度10 print("命令返回长度",cmd_res_size)11 received_size=012 #received_data=http://www.mamicode.com/b‘‘#接收返回的数据13 while received_size <int(cmd_res_size.decode()):14 data=http://www.mamicode.com/client.recv(1024)#接收客户端返回的消息15 received_size+=len(data)#每次收到的数据大小相加16 print(data.decode())#打印数据17 else:18 print("cmd res recevise done",received_size)19 #data=http://www.mamicode.com/data.decode("utf-8")20 #print(data.decode())21 client.close()
当然,咱们现在处理的任务还是单用户,如果有多用户呢?怎么弄,就用到socketserver了、
二、socketserver
socketserver内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求
1、ThreadingTCPServer
使用ThreadingTCPServer:
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
- socketserver基本代码:
1 import socketserver 2 3 class MyTCPHandler(socketserver.BaseRequestHandler): 4 """ 5 The request handler class for our server. 6 7 It is instantiated once per connection to the server, and must 8 override the handle() method to implement communication to the 9 client.10 """11 12 def handle(self):13 # self.request is the TCP socket connected to the client14 self.data = http://www.mamicode.com/self.request.recv(1024).strip()15 print("{} wrote:".format(self.client_address[0]))16 print(self.data)17 # just send back the same data, but upper-cased18 self.request.sendall(self.data.upper())19 20 if __name__ == "__main__":21 HOST, PORT = "localhost", 999922 23 # Create the server, binding to localhost on port 999924 server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)25 26 # Activate the server; this will keep running until you27 # interrupt the program with Ctrl-C28 server.serve_forever()
用socketserver对ssh程序做修改,实现多用户同时操作互不影响
PythonDay8