什么是 Socket

Socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

所以,我们无需深入理解 TCP/UDP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然就是遵循 TCP/UDP 标准的。

套接字的分类:

  基于文件类型的套接字家族:AF_UNIX(在 Unix 系统上,一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程同时运行在同一机器,可以通过访问同一个文件系统间接完成通信)

  基于网络类型的套接字家族:AF_INET(Python 支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用 AF_INET)

基于 TCP 协议的 socket

工作流程:

下面我们举个打电话的小例子来说明一下

如果你要给你的一个朋友打电话,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。

(如果你去一家餐馆吃饭,假设那里的老板就是服务端,而你自己就是客户端,当你去吃饭的时候,你肯定的知道那个餐馆,也就是服务端的地址,但是对于你自己来说,餐馆的老板不需要知道你的地址)

服务端
1)创建套接字描述符(socket)
2)设置服务器的 IP 地址和端口号(需要转换为网络字节序的格式)
3)将套接字描述符绑定到服务器地址(bind)
4)将套接字描述符设置为监听套接字描述符(listen),等待来自客户端的连接请求,监听套接字维护未完成连接队列和已完成连接队列
5)从已完成连接队列中取得队首项,返回新的已连接套接字描述符(accept),如果已完成连接队列为空,则会阻塞
6)从已连接套接字描述符读取来自客户端的请求(read / recv)
7)向已连接套接字描述符写入应答(write / send)
8)关闭已连接套接字描述符(close),回到第 5 步等待下一个客户端的连接请求

服务端必须满足至少三点:

  1)绑定一个固定的 IP 和端口号

  2)一直对外提供服务,稳定运行

  3)能够支持并发

客户端:
1)创建套接字描述符(socket)
2)设置服务器的 IP 地址和端口号(需要转换为网络字节序的格式)
3)请求建立到服务器的 TCP 连接并阻塞,直到连接成功建立(connect)
4)向套接字描述符写入请求(write / send)
5)从套接字描述符读取来自服务器的应答(read / recv)
6)关闭套接字描述符(close)

import socket
socket.socket(socket_family, socket_type, proto=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。proto 一般不填,默认值为 0。 获取TCP/IP套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 获取UDP/IP套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket模块函数用法

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 1. 服务端套接字函数
phone.bind('主机ip地址', 端口号) # 绑定到(主机,端口号)套接字
phone.listen() # 开始TCP监听
phone.accept() # 被动接受TCP客户的连接,等待连接的到来

服务端套接字函数

# 2. 客户端套接字函数
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
phone.connect() # 主动连接服务端的ip和端口
phone.connect_ex() # connect()函数的扩展版本,出错的时候返回错码,而不是抛出异常

客户端套接字函数

# 3. 服务端和客户端的公共用途的嵌套字函数
phone.recv() # 接受TCP数据
phone.send() # 发送TCP数据
phone.recvfrom() # 接受UDP数据
phone.sendto() # 发送UDP数据
phone.getpeername() # 接收到当前套接字远端的地址
phone.getsockname() # 返回指定套接字的参数
phone.setsockopt() # 设置指定套接字的参数
phone.close() # 关闭套接字

服务端和客户端的公共用途的嵌套字函数

# 面向锁的套接字方法
phone.setblocking() # 设置套接字的阻塞与非阻塞模式
phone.settimeout() # 设置阻塞套接字操作的超时时间
phone.gettimeout() # 得到阻塞套接字操作的超时时间

面向锁的套接字方法

# 面向文件的套接字函数
phone.fileno() # 套接字的文件描述符
phone.makefile() # 创建一个与该套接字相关的文件

面向文件的套接字函数

TCP是基于链接的,必须先启动服务器,然后再启动客户端去链接服务端

简单版

import socket

# 1. 创建套接字描述符, 用来建立链接
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(phone) # 2. 设置IP和端口号, 绑定套接字描述符
phone.bind(("127.0.0.1", 8080)) # 3. 将套接字描述符设置为监听状态, 设置同一时刻最大请求数为5
phone.listen(5) print("start...")
# 4. 等待来自客户端的连接
conn, client_addr = phone.accept()
# accept有返回值,是一个元组
# 元组的第一个参数是双向链接的套接字对象(即三次握手的结果), 用来收发消息
# 第二个参数是一个元组,存放客户端的IP和端口号
# print(conn)
# print(client_addr) # 5. 收/发消息, 1024是接收的最大字节数bytes
data = conn.recv(1024)
print("收到客户端的数据", data)
conn.send(data.upper()) # 6. 关闭双向链接的套接字对象
conn.close() # 7. 关闭套接字描述符
phone.close()

服务端

import socket

# 1. 创建套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 连接服务端的IP地址和端口号
phone.connect(("127.0.0.1", 8080)) # 3. 发/收消息
phone.send("hello".encode("utf-8")) # 只能发bytes类型
data = phone.recv(1024)
print("收到服务端的消息", data) # 4. 关闭套接字描述符
phone.close()

客户端

由于 socket 模块中有太多的属性。在这里破例使用了 'from module import *' 语句。使用 'from socket import *',就把 socket 模块里的所有属性都带到命名空间里了,这样能大幅减短代码。
例如 tcpSock = socket(AF_INET, SOCK_STREAM)

通信循环

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5) conn, client_addr = server.accept() # 通信循环
while True:
data = conn.recv(1024)
conn.send(data.upper()) conn.close()
server.close()

服务端

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080)) # 通信循环
while True:
msg = input("请输入: ").strip()
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data) client.close()

客户端

但是这样写有一个 bug,当你手动结束客户端的程序运行时,服务端也会跟着崩溃

因为 conn 代表的是一个双向连接,只有服务端和客户端都正常运行的时候,conn 才有意义,然而此时客户端是非正常的断开,服务端还在使用没有意义的 conn 做 recv 操作,无法收到消息,所以在 Windows 上直接崩溃,而在 Linux 上,相同的操作服务端会一直处于收空的状态

补救措施是,在 Windows 系统上捕捉异常,在 Linux 系统上加上判断

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5) conn, client_addr = server.accept() # 通信循环
while True:
try:
data = conn.recv(1024)
# 针对Linux系统
if len(data) == 0:
break
conn.send(data.upper()) except ConnectionResetError:
break conn.close()
server.close()

服务端

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080)) # 通信循环
while True:
msg = input("请输入: ").strip()
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data) client.close()

客户端

链接通信循环

这样虽然解决了崩溃问题,但是当手动结束客户端时,服务端还是会跟着结束,所以在服务端等待客户端的连接前加上循环,从而达到 “链接 + 通信” 循环

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5) # 链接循环
while True:
conn, client_addr = server.accept() # 通信循环
while True:
try:
data = conn.recv(1024)
# 针对Linux系统
if len(data) == 0:
break
conn.send(data.upper()) except ConnectionResetError:
break conn.close()
server.close()

服务端

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080)) # 通信循环
while True:
msg = input("请输入: ").strip()
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data) client.close()

客户端

但这样做,服务端每次只能针对于一个客户端,只有当这个客户端的收发消息结束后才能给下一个客户端服务,无法达到并发的效果,这个后面学到并发时再讲

其实还有一个问题,当客户端传一个空消息时,会发生阻塞状态,因为发空的时候服务端时无法收到的(空时是什么都没有),服务端收不到,无法返回给客户端,所以客户端处于阻塞状态。补救方法是不让客户端输入空

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080)) # 通信循环
while True:
msg = input("请输入: ").strip()
if len(msg) == "":
continue
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data) client.close()

客户端

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5) # 连接循环
while True:
conn, client_addr = server.accept() # 通信循环
while True:
try:
data = conn.recv(1024)
# 针对Linux系统
if len(data) == 0:
break
conn.send(data.upper()) except ConnectionResetError:
break conn.close()
server.close()

服务端

 模拟ssh实现远程执行命令

当使用客户端远程连接服务器时,在客户端上执行命令,服务器会返回命令执行的结果给客户端,那么该如何实现呢?

from socket import *
import subprocess server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5) # 连接循环
while True:
conn, client_addr = server.accept() # 通信循环
while True:
try:
cmd = conn.recv(1024) # cmd = b'dir'
# # 针对Linux系统
if len(cmd) == 0:
break
# 命令的执行结果
obj = subprocess.Popen(cmd.decode("utf-8"),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
conn.send(stdout + stderr) except ConnectionResetError:
break conn.close()
server.close()

服务端

import socket

# 1. 创建套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 连接服务端的IP地址和端口号
phone.connect(("127.0.0.1", 8080)) # 3. 发/收消息
phone.send("hello".encode("utf-8")) # 只能发bytes类型
data = phone.recv(1024)
print("收到服务端的消息", data) # 4. 关闭套接字描述符
phone.close()

客户端

但是目前这样有一个局限性,我将接收端数据的最大字节数设置为1024,当发送端发的数据量小于接收端的1024时,可以被完全接收,但是发送端的数据量大于1024时,就只能接收1024条数据,那么多出的那些数据该如何处理呢?

首先客户端发送一条执行命令给服务端,让服务端接收,这里命令的字节数大多数情况不会大于1024,所以可以被完全接收,暂不考虑,当服务端接收了命令执行后,会将命令的执行结果发送给客户端,让客户端接收,这里命令的执行结果是很有可能大于1024个字节的,例如:tasklist,在终端上显示的最后一条是自己,而在上面所写的两个文件中只能显示几条结果,很显然是大于1024的

但这时再输入 dir 时,竟然是 tasklist 没有执行完的继续显示,再输入其它命令,还是 tasklist 没有执行完的继续显示,这发生了什么?

这就是待解决的粘包问题,下一节将会学习

Learning-Python【28】:基于TCP协议通信的套接字的更多相关文章

  1. Learning-Python【30】:基于UDP协议通信的套接字

    UDP协议没有粘包问题,但是缓冲区大小要足够装数据包大小,建议不要超过 512 服务端: # 服务端 import socket server = socket.socket(socket.AF_IN ...

  2. 基于TCP协议的socket套接字编程

    目录 一.什么是Scoket 二.套接字发展史及分类 2.1 基于文件类型的套接字家族 2.2 基于网络类型的套接字家族 三.套接字工作流程 3.1 服务端套接字函数 3.2 客户端套接字函数 3.3 ...

  3. python中基于tcp协议的通信(数据传输)

    tcp协议:流式协议(以数据流的形式通信传输).安全协议(收发信息都需收到确认信息才能完成收发,是一种双向通道的通信) tcp协议在OSI七层协议中属于传输层,它上承用户层的数据收发,下启网络层.数据 ...

  4. 基于TCP连接的socket套接字编程

    基于TCP协议的套接字编程(简单) 服务端 import socket server = socket.socket() server.bind( ('127.0.0.1', 9999) ) serv ...

  5. python基础22------python基础之基于tcp和udp的套接字

    一.TCP套接字 1.low版tcp套接字 服务器端 客户端 2.改进版tcp套接字 服务端 客户端 二.UDP的套接字 服务器 客户端 注:udp的套接字可以支持多个客户端同时访问,但tcp套接字就 ...

  6. Python 31 TCP协议 、socket套接字

    1.TCP协议 可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割. (1)三次握手建链接( ...

  7. 网络编程之基于tcp和udp的套接字

    一   udp协议网络编程 DNS:将域名解析成ip地址 SOCK_DGRAM:数据报协议,也是udp协议 udp协议的网络编程的一些用法: recvfrom:接收消息,接收的时一个元组,元组里面的元 ...

  8. 基于UDP协议的socket套接字编程

    目录 一.UDP套接字简单示例 1.1 服务端 二.客户端 三.UPD套接字无粘包问题 3.1 服务端 3.2 客户端 四.qq聊天 4.1 服务端 4.2 客户端1 4.3 客户端2 4.4 运行结 ...

  9. python 30 基于TCP协议的socket通信

    目录 1. 单对单循环通信 2. 循环连接通信:可连接多个客户端 3. 执行远程命令 4. 粘包现象 4.1 socket缓冲区 4.2 出现粘包的情况: 4.3 解决粘包现象 bytes 1. 单对 ...

随机推荐

  1. Scrapy框架基本使用

    pycharm+Scrapy 距离上次使用Scrapy已经是大半年前的事情了,赶紧把西瓜皮捡回来.. 简单粗暴上爬取目标: 初始URL:http://quotes.toscrape.com/ 目标:将 ...

  2. ps把照片背景变成透明[原为白色或其他颜色]

    在第六步:魔法棒选中之后,按delete键,即可!!! 注意:背景变成透明颜色,需要把照片格式变成png 就可以了!!!

  3. 再次重温《Right here waiting》

    记得高中时候听到这首曲子(当时还让同桌帮我抄了这首曲子,后来这个本子也不知道扔到哪里去了), 前天偶尔在虾米遇到这首曲子,过去的青涩岁月历历在目,自己手动打打歌词,一方面是为了重温这首曲子,另一方面, ...

  4. String类,ThreadLocal

    1,StringBuffer修改缓冲区的方法是同步的            单任务使用StringBuilder更有效 2,replace和replaceAll方法区别 3,spilt方法 a,b,c ...

  5. ubuntu下安装thrift

    configure: error: "Error: libcrypto required."

  6. pytorch 中的重要模块化接口nn.Module

    torch.nn 是专门为神经网络设计的模块化接口,nn构建于autgrad之上,可以用来定义和运行神经网络 nn.Module 是nn中重要的类,包含网络各层的定义,以及forward方法 对于自己 ...

  7. Python解析Linux命令行

    写了个python脚本在linux需要传入参数使用,python参数传入有几个方法, 先用了Python中命令行参数的最传统的方法sys.argv linux cmd ~& python ma ...

  8. C和C指针小记(十三)-数组

    1.1 一维数组 一维数组的声明: int a[10]; 这里a就是一个数组. 数组a的类型就是一个指向整型的常量指针. 但是数组和指针是**不相同**的. **数组具有特定数量的元素,而指针只是一个 ...

  9. CSS-对于IE的兼容问题处理

    css兼容问题 兼容问题 1.文字本身的大小不兼容.同样是font-size:14px的宋体文字,在不同浏览器下占的空间是不一样的,ie下实际占高16px,下留白3px,ff下实际占高17px,上留白 ...

  10. ARIMA模型总结

    时间序列建模基本步骤 获取被观测系统时间序列数据: 对数据绘图,观测是否为平稳时间序列:对于非平稳时间序列要先进行d阶差分运算,化为平稳时间序列: 经过第二步处理,已经得到平稳时间序列.要对平稳时间序 ...