TCP/IP
要想理解socket首先得熟悉一下TCP/IP协议族, TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何再它们之间传输的标准,
从字面意思来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于OSI模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的
估计有兴趣打开此文的同学都对此有一定了解了,加上我也是一知半解,所以就不详细解释,有兴趣同学可以上网上搜一下资料
socket
我们知道两个进程如果需要进行通讯最基本的一个前提能够标识唯一的一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket和file的区别:
- file模块是针对某个指定文件进行【打开】【读写】【关闭】
- socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
以使用TCP协议通讯的socket为例,其交互流程大概是这样子的:
图一
服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
服务器为socket绑定ip地址和端口号
服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开
客户端创建socket
客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket
服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
客户端连接成功,向服务器发送连接状态信息
服务器accept方法返回,连接成功
客户端向socket写入信息
服务器读取信息
客户端关闭
服务器端关闭
这里还有个很重要的概念,在TCP中是通过三次握手建立连接,四次握手释放连接
TCP用三路握手(three-way handshake)过程创建一个连接。在连接创建过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。
三次握手(相当与图一的建立连接,sockect把过程封装了):
一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端打开一个套接字(socket)然后监听来自另一方的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,用户端就能开始创建主动打开(active open)。
- 客户端通过向服务器端发送一个SYN来创建一个主动打开,作为三路握手的一部分。客户端把这段连接的序号设定为随机数A。
- 服务器端应当为一个合法的SYN回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身又有一个随机序号B。
- 最后,客户端再发送一个ACK。当服务端受到这个ACK的时候,就完成了三路握手,并进入了连接创建状态。此时包序号被设定为收到的确认号A+1,而响应则为B+1。
四次释放(相当于图一的释放连接):
连接终止使用了四路握手过程(four-way handshake),在这个过程中每个终端的连接都能独立地被终止。因此,一个典型的拆接过程需要每个终端都提供一对FIN和ACK。
图示过程如下:
-
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
-
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
-
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
-
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
python中相关函数:
先看个简单的socket例子:
1 import socket 2 3 addr = ("localhost",1024) 4 5 client = socket.socket() #创建一个套接字实例 6 client.connect(addr) #将套接字连接到远程地址 7 8 9 client.send("Hello World!".encode()) #发送数据,只能是bytes形式 10 data = client.recv(1024) #接收数据 11 print("recv:%s"%data.decode()) #打印数据 12 13 client.close() #关闭连接
1 import socket 2 3 addr = ("localhost",1024) 4 5 server = socket.socket() #创建一个socket实例 6 7 server.bind(addr) #绑定套接字绑定到本地地址,格式为(地址,端口) 8 server.listen() #监听端口 9 conn,addr = server.accept() #等待请求 10 print(conn,addr)#conn就是客户端连接过来而在服务器端为其生成的一个实例,addr客户端地址,格式为(地址,端口) 11 12 13 data = conn.recv(1024) #接收数据 14 print("用户端数据>>", data.decode()) 15 conn.send("收到了".encode()) #发送数据 16 17 server.close() #关闭连接
sk = socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) -> socket object
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().
翻译:用一个给定的地址簇,套接字类型和协议号创建一个新的套接字,地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。协议号通常为0,可以省略,或则在地址簇为AF_CAN的情况下,协议应为CAN_RAW 或则CAN_BCM的其中之一。如果指定fileno,其它参数将被忽略,导致带有特殊的文件描述符的套接字返回,与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。 这可能有助于使用socket.close()关闭一个分离的套接字。
参数一:地址簇
socket.AF_INET IPv4(默认)
socket.AF_INET6 IPv6
socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
参数二:类型
socket.SOCK_STREAM 流式socket , for TCP (默认)
socket.SOCK_DGRAM 数据报式socket , for UDP
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
socket.SOCK_SEQPACKET 可靠的连续数据包服务
参数三:协议
0 (默认)与特定的地址家族相关的协议,如果是 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)
发送文件 ,但目前多数情况下并无什么卵用。
一个简单的sockct交互例子
1 import socket 2 3 addr = ("localhost",1024) 4 5 client = socket.socket() #创建一个套接字实例 6 client.connect(addr) #将套接字连接到远程地址 7 8 while True: 9 msg = input(">>").strip() 10 if len(msg) == 0:continue #不能发送空的数据 11 client.send(msg.encode()) #发送数据,只能是bytes形式 12 data = client.recv(1024) #接收数据 13 print("recv:%s"%data.decode()) #打印数据 14 15 16 client.close() #关闭连接
1 import socket 2 3 addr = ("localhost",1024) 4 5 server = socket.socket() #创建一个socket实例 6 7 server.bind(addr) #绑定套接字绑定到本地地址,格式为(地址,端口) 8 server.listen(5) #监听端口 9 while True: 10 conn,addr = server.accept() #等待请求 11 print(conn,addr)#conn就是客户端连接过来而在服务器端为其生成的一个实例,addr客户端地址,格式为(地址,端口) 12 13 while True: 14 data = conn.recv(1024) #接收数据 15 if not data: #对关闭的连接进行处理 16 print("client is lost...") 17 break 18 print("用户端数据>>", data.decode()) 19 conn.send("收到了".encode()) #发送数据 20 21 server.close() #关闭连接
这个程序在window下执行有点问题,建议在linux运行
一个简单的SSH例子
1 import socket 2 import sys 3 addr = ("localhost",6667) 4 5 client = socket.socket() #创建一个套接字实例 6 client.connect(addr) #将套接字连接到远程地址 7 8 while True: 9 msg = input(">>").strip() 10 if len(msg) == 0:continue 11 client.send(msg.encode()) #发送数据,只能是bytes形式 12 data = client.recv(102400) #接收数据 13 if data.decode() == 0: 14 continue 15 sys.stdout.write(data.decode()) #打印数据rt socket 16 17 18 client.close() #关闭连接
1 import socket 2 import subprocess 3 import os 4 addr = ("localhost",6667) 5 6 server = socket.socket() #创建一个socket实例 7 8 server.bind(addr) #绑定套接字绑定到本地地址,格式为(地址,端口) 9 server.listen() #监听端口 10 while True: 11 conn,addr = server.accept() #等待请求 12 print(conn,addr)#conn就是客户端连接过来而在服务器端为其生成的一个实例,addr客户端地址,格式为(地址,端口) 13 14 while True: 15 data = conn.recv(1024).decode() #接收数据 16 if not data: 17 print("client is lost...") 18 break 19 print("收到命令:",data) 20 if data.startswith("cd "): #如果是切换目录cd,执行完就完了,并不会执行该命令,需使用os.chdir 21 cont = data.split() 22 if os.path.isdir(cont[-1]): 23 os.chdir(cont[-1]) 24 conn.send('0'.encode()) 25 continue 26 res = subprocess.run(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 27 if res.stdout: 28 conn.send(res.stdout) #发送数据 29 elif res.stderr: 30 conn.send(res.stderr) 31 else: 32 conn.send(str(res.returncode).encode()) 33 server.close() #关闭连接
very cool , 这样我们就做了一个简单的ssh , 但多试几条命令你就会发现,上面的程序有以下2个问题。
- 不能执行top等类似的 会持续输出的命令,这是因为,服务器端在收到客户端指令后,会一次性通过subprocess.run执行,并得到结果后返回给客户,但top这样的命令用此执行你会发现永远都不会结束,所以客户端也永远拿不到返回。(真正的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的文件并返回,客户端怎么才能完整的接受到呢?那就只能循环收取啦。