什么是 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. BOM设计的一些问题及解决方案探讨----合版BOM

    BOM是ERP的核心资料,也是比较难的一块,不仅涉及的内容多,要求准确性高,时效性也要求高.但传统的ERP在处理BOM时有不少问题,因此也有些软件公司引入了各种BOM类型,像"标准BOM&q ...

  2. vlan之间的通信-单臂路由与三层交换之间的互通

    注:本试验基于单臂路由通信,三层交换通信,请完成以上两个实验,并保证能够通信 熟练掌握单臂路由的配置 熟练掌握三层交换的配置 三层交换与单臂路由的互通 实验原理 三层交换机在原有二层交换机的基础之上增 ...

  3. Changing Ethernet Media Speed for AIX

    ITS UNIX Systems Changing Ethernet Media Speed for AIX First you need to find out the device name of ...

  4. MySQL优化小结

    数据库的配置是基础.SQL优化最重要(贯穿始终,每日必做),由图可知,越往上优化的面越小,最基本的SQL优化是最重要的,往上各个参数也没太多调的,也不可能说调一个innodb参数性能就会好多少,而动不 ...

  5. xcode代码提示没了

    defaults write com.apple.dt.XCode IDEIndexDisable 0 https://www.jianshu.com/p/57a14bed9d1b

  6. sourceTree回退撤销commit

    https://blog.csdn.net/gang544043963/article/details/71511958

  7. LeetCode 696 Count Binary Substrings 解题报告

    题目要求 Give a string s, count the number of non-empty (contiguous) substrings that have the same numbe ...

  8. (备忘)怎么去除WinRAR弹窗广告?

    1.在WinRAR的安装目录下新建一个记事本,命名为“rarreg.key”. 2.打开记事本,将一下内容复制进去. RAR registration data Federal Agency for ...

  9. Java文件写入与读取实例求最大子数组

    出现bug的点:输入数组无限大: 输入的整数,量大: 解决方案:向文件中输入随机数组,大小范围与量都可以控制. 源代码: import java.io.BufferedReader; import j ...

  10. docker centos 老是退出

    1. 使用docker 镜像可以加快拉去.操作系统的使用第二种格式. 您可以使用以下命令直接从该镜像加速地址进行拉取: $ docker pull registry.docker-cn.com/myn ...