source: alex - day 8 - socket编程进阶
网络架构的基础知识
计算机如何实现通信?是通过网卡,连上线,就可以实现通信,数据可传输。 数据的形式各异,包括图片,视频等。
不通类型的通信(email,收发图片等),是通过不同类型的协议来支持。通过协议,交换不通的数据流。
协议 | 作用 |
---|---|
http | 网站 |
smtp | |
dns | 将域名解析成ip地址 |
ftp | 下载文件 |
ssh | |
smp | 简单网络监控 |
icmp | ping包,测试网络通不通 (网络层) |
dhcp | ip地址分配 |
... | ... |
数据的交换,无非就是两种方式:发(send) 和 收 (recieve)
OSI七层模型
- 应用
- 表示
- 会话
- 传输:能够传输的要求,满足 tcp/IP (三次握手,四次断开) 和 udp (不安全的数据传输,不关心对方是否在。一般很少用) 两种协议。同时,也必须标准的数据类型。
- 网络:认识ip地址
- 数据链路:只认识mac地址(物理地址:16进制的地址),不认识ip地址。
- 物理层
基本上的应用协议都在tcp/ip之上,icmp在网络层,其他基本都在应用层。不同的协议就像不通的语言。 本质上,不通的协议,都是发一条消息收一条消息。那么如果每个协议都需要自己在计算机底层上实现发和收(二进制),那就太麻烦了并且得深刻理解tcp/ip的基层原理。所以将复杂的东西进行封装,并只暴露给用户一个接口,让其他协议直接调用即可。 这个收和发的封装就叫做socket,它是所有的网络协议的基础。socket只干两件事,收 和 发。
详细介绍: OSI七层模型与TCP/IP五层模型 OSI七层模型详解
socket
运行原理
如何在两台机器中实现通信呢?
假设有A和B两个机器,B机器访问A机器。在访问时,机器B提供其ip地址和程序端口。 A机器上有很多程序(每个机器可开放65535个端口),假设有nginx(网站), mysql, alexTv等。 其流程是,B通过ip地址找到A机器,接着通过端口号找到需要访问的程序并擦作, 比如默认80就是nginx, mysql 3306等。 通过3306连上mysql, 数据返回机器B。B机器可能有多个程序包括qq, ssh, weixin等,通过最初的ip地址和端口返回。
socket是对所有上层协议的封装, 封装了tcpip及udp等
套接字 socket: 通信端点
套接字是计算机网络数据结构, 体现了“通信端点”的概念。建立一个socket必须至少有2端,一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求,连接建立之后,双方可以互发数据。 从上面来看,任何类型的通信开始之前,网络应用程序必须创建套接字。最初,套接字存在于同一主机中两个运行的程序进行通信,这就是进程间通信。套接字有两种类型:基于文件和面对网络的。
socket families 地址簇
地址家族 | 描述 |
---|---|
socket.AF_UNIX | 本即进程间通讯,基于文件的 |
socket.AF_INET | 因特网,基于网络的,使用最广泛 |
socket.AF_INET6 | 第6版因特网协议, 基于网络的 |
其他还有,AF_NETLINK, AF_TIPC
socket types 类型
不管用哪种socket families, 都会有两种不通风格的套接字连接。
- 面向连接的 - 虚拟电路 or 流套接字
意思是进行通信之前必须先建立连接。面对连接的通信提供序列化的,可靠的,不重复的数据交付,而没有记录边界。 实现这种连接类型的主要协议是TCP。为了创建TCP套接字,必须采用SOCK_STREAM作为套接字类型
- 无连接的套接字 - 数据报类型
不用连接。但数据传输的过程中,无法保证顺序性、可靠性或重复性, 但保存了记录边界,也就是说是整体发送,不是分成多个片段的。数据报的成本更低。 数据报连接类型的主要协议是UDP。创建UDP套接字,必须采用SOCK_DGRAM
- 其他
SOCK_RAW 原始套接字,普通的套接字无法处理ICMP, IGMP等网络报文,但是SOCK_RAW可以。也可以处理IPv4报文。 SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
socket() 模块函数 - 创建套接字
- socket.socket(socket_family, socket_type, protocol=0)
# 创建TCP/IP套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 或在python 3.x, tcp/ip为默认 socket.socket() # 创建UDP/IP套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- socket属性直接引入命名空间
from socket import * tcpSock = socket(AF_INET, SOCK_STREAM)
套接字对象(内置)方法
名称 | 描述 |
---|---|
服务器套接字方法 | |
s.bind() | 将地址(主机名,端口号对)绑定到套接字上 |
s.listen() | 设置并启动TCP监听器 |
s.accept() | 被动接受TCP客户端连接,一直等待直到连接达到(阻塞)。返回两个值:连接的标记 和 对方的IP地址 |
客户端套接字方法 | |
s.connect() | 主动发起TCP服务器连接 |
s.connect_ex() | connect()扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常 |
普通的套接字方法 | |
s.recv() | 接受TCP消息 |
s.recv_into() | 接受TCP消息到指定的缓冲区 |
s.send() | 发送TCP消息 |
s.sendall() | 完整的发送TCP消息 |
s.recvfrom() | 接受UDP消息 |
s.recvfrom_into() | 接受UDP消息到指定的缓冲区 |
s.sendto() | 发送UDP消息 |
s.getpeername() | 连接到TCP的远程地址 |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回给定套接字选项的值 |
s.setsockopt() | 设置给定套接字选项的值 |
s.shutdown() | 关闭连接 |
s.close() | 关闭套接字 |
s.detach() | 在未关闭文件描述符的情况下关闭套接字,返回文件描述符 |
s.ioctl() | 控制套接字模式(只支持windows) |
面向阻塞的套接字方法 | |
s.setblocking() | 设置套接字的阻塞或非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 获取阻塞套接字操作的超时时间 |
面向文件的套接字方法 | |
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建与套接字关联的文件对象 |
数据属性 | |
s.family | 套接字家族 |
s.type | 套接字类型 |
s.proto | 套接字协议 |
socket模块属性
除了socket.socket(), socket模块的其他属性
属性名称 | 描述 |
---|---|
数据属性 | |
AF_UNIX, AF_INET, AF_INET6、AF_NETLINK、AF_TIPC | Python 支持的套接字家族 |
SO_STREAM, SO_DGRAM | 套接字类型 (TCP = 流, UDP = 数据报) |
has_ipv6 | 表示是否支持IPv6 的布尔标志 |
异常 | |
error | 套接字相关错误 |
herrora | 主机和地址相关的错误 |
gaierror | 地址相关的错误 |
timeout | 超时 |
函数 | |
socket() | 用指定的地址家族,套接字类型和协议类型(可选)创建一个套接字对象 |
socketpair() | 用指定的地址家族,套接字类型和协议类型(可选)创建一对套接字对象 |
create_connection() | 常规函数, 它接受一个地址(主机名,端口名)对,返回套接字对象 |
fromfd() | 用一个已经打开的文件描述符创建一个套接字对象 |
ssl() | 在套接字初始化一个安全套接字层(SSL)。不做证书验证。 |
getaddrinfo() | 得到地址信息 |
getnameinfo() | 给定一个套接字地址,返回(主机名,端口号)二元组 |
getfqdn() | 返回完整的域的名字 |
gethostname() | 得到当前主机名 |
gethostbyname() | 由主机名得到对应的ip 地址 |
gethostbyname_ex() | gethostbyname()的扩展版本,返回主机名,主机所有的别名和IP 地址列表。 |
gethostbyaddr() | 由IP 地址得到DNS 信息,返回一个类似gethostbyname_ex()的3 元组。 |
getprotobyname() | 由协议名(如'tcp')得到对应的号码。 |
getservbyname()/getservbyport() | 由服务名得到对应的端口号或相反,两个函数中,协议名都是可选的。 |
ntohl()/ntohs() | 把一个整数由网络字节序转为主机字节序 |
htonl()/htons() | 把一个整数由主机字节序转为网络字节序 |
inet_aton()/inet_ntoa() | 把IP 地址转为32 位整型,以及反向函数。(仅对IPv4 地址有效) |
inet_pton()/inet_ntop() | 把IP 地址转为二进制格式以及反向函数。(仅对IPv4 地址有效) |
getdefaulttimeout()/ setdefaulttimeout() | 得到/设置默认的套接字超时时间,单位秒(浮点数) |
socket 网络编程
创建tcp服务器的逻辑
import socket
socket.TCP/IP # 定义操作类型
connect(a.ip, a.port) # 访问A
socket.send("hello")
接收端的伪代码:
import socket
socket.TCP/IP
listen(0.0.0.0, 6969) # 具体的网卡的ip, 6969端口号
waiting() # 为了不让数据卡着
recv()
send
最后,发送端
socket.close()
示例1: 最简单的连接 - 客户端和服务器运行在一台计算机上。
import socket client = socket.socket() # 创建套接字,声明socket类型,同时生成socket连接对象 client.connect(("localhost", 6969)) client.send(b"hello world") # python 3.x只能发bytes类型,只能接受ascii码类型;所以不能直接传中文。 传中文见下 # client.send("如何传中文呢?",encode("utf-8")) data = client.recv(1024) print("recv", data) client.close()
import socket server = socket.socket() server.bind(("localhost", 6969)) # 绑定监听的端口 server.listen() # 监听 print("start to listen") conn, addr = server.accept() # 等电话,返回连接的标记位 和 对方的ip地址 # conn就是客户端连过来而在服务器端为其生成的一个连接实例 print(conn, addr) print("connected") data = conn.recv(1024) print("recv", data) # 传中文: print("recv", data.decode()) conn.send(data.upper()) server.close()
提示:
- 值示主机“localhost”的代码和输出, 或者看到“127.0.0.1”的ip地址,这是代表客户端和服务器在一台计算机上运行。
- 传中文时,要encode和decode
以上代码有个问题,就是连接到一个客户端时,只能接受一个request就跳出了。之后需要再次连接。
示例3: 最简单完整的通信连接示例
目的:创建一个tcp服务器,它接受来自客户端的消息,然后将消息加上时间戳前缀并发送回客户端。
from socket import * from time import ctime HOST = '' PORT = 21567 BUFSIZ = 1024 ADDR = (HOST,PORT) tcpSerSock = socket(AF_INET, SOCK_STREAM) # 等于 tcpSerSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 因为上文 from socket import *, 所以已经socket属性引入到了命名空间。 tcpSerSock.bind(ADDR) tcpSerSock.listen(5) while True: print("waiting for connection...") tcpCliSock, addr = tcpSerSock.accept() print("...connected from:", addr) while True: data = tcpCliSock.recv(BUFSIZ) if not data: break tcpCliSock.send(b'[%s] %s' %( bytes(ctime(), 'utf-8'), data)) tcpCliSock.close() tcpSerSock.close()
from socket import * HOST = "127.0.0.1" # or "localhost" PORT = 21567 BUFSIZ = 1024 ADDR = (HOST, PORT) tcpCliSock = socket(AF_INET, SOCK_STREAM) tcpCliSock.connect(ADDR) while True: data = input(">>>:") if not data: break tcpCliSock.send(data.encode()) data = tcpCliSock.recv(BUFSIZ) if not data: break print(data.decode("utf-8")) tcpCliSock.close()
示例4: 模拟ssh访问
以下代码,是在Linux下环境,而且还是python2.7的环境,如果是python 3的话,需要编码和解码。
1. server端
import socket,os #导入os模块 sever = socket.socket() sever.bind(("127.0.0.1",6969)) sever.listen(5) #最大允许有多少个链接 while True: conn,address = sever.accept() print("电话来了") count = 0 while True: data = conn.recv(1024) if not data:break res = os.popen(data).read() # 调用linux命令 conn.send(res) #执行的命令返回值 sever.close()
2. 客户端
import socket client = socket.socket() client.connect(("localhost",6969)) while True: msg = raw_input(">>>:") if len(msg) == 0:continue client.send(msg) data = client.recv(1024) print(data) client.close()
conn.send(res)这边如果需要发送全部的话,需要conn.sendall(res)
示例5: 下载文件
1. server端
import socket sever = socket.socket() sever.bind(("127.0.0.1",6969)) sever.listen() conn,address = sever.accept() print("电话来了") while True: data = conn.recv(1024) if not data: print("数据为空") break with open("test","rb") as test_file: all_data_bytes = test_file.read() #读取需要下载的文件,发送给客户端 conn.sendall(all_data_bytes) sever.close()
2. 客户端
import socket client = socket.socket() client.connect(("localhost",6969)) while True: msg = input(">>>:") if len(msg) == 0:continue client.send(msg.encode()) data = client.recv(1024000) #这边设置的大一点,防止文件的内容接收不到 with open("test_put","wb") as test_put_file: #把下载下来的内容写入到文件中 test_put_file.write(data) client.close()
注意:这边客户端的接收时有限制的,如果超出了客户端的限制,客户端只接收自己的一部分,而剩余的会在还是在缓冲去,下一次服务端再send的时候,不会发新数据,先把缓冲区剩下的数据发送到客户端。