什么是 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. 大臣的旅费---树的直径(dfs)

    很久以前,T王国空前繁荣.为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市. 为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者 ...

  2. C#在WinForm下使用HttpWebRequest上传文件

    转自:http://blog.csdn.net/shihuan10430049/article/details/3734398 这段时间因项目需要,要实现WinForm下的文件上传,个人觉得采用FTP ...

  3. Linux命令vi/vim 使用方法讲解

    vi/vim 基本使用方法 vi编辑器是所有Unix及Linux系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令.由于对Unix及Linux系统的 ...

  4. 使用强类型DataSet增加数据并获取自动增长的ID

    使用强类型的DataSet可以方便的操作数据库:有时候我们会对表的ID设置为自动增长,并且需要在插入数据后获取新插入数据的ID,按以下方法即可达到目的: 一.     首先建立一个表,id为自动增加, ...

  5. Codeforces 1089K - King Kog's Reception - [线段树][2018-2019 ICPC, NEERC, Northern Eurasia Finals Problem K]

    题目链接:https://codeforces.com/contest/1089/problem/K time limit per test: 2 seconds memory limit per t ...

  6. PHP实现数组中每个字符出现次数最多的,并且如果两个字符出现相同,则在前面的先输出功能

    $arr = ['a', 'b', 'a', 'e', 'g', 'g', 'a']; $count_per_values = array_count_values($arr); $res = []; ...

  7. LeetCode 237 Delete Node in a Linked List 解题报告

    题目要求 Write a function to delete a node (except the tail) in a singly linked list, given only access ...

  8. time和datetime和tzinfo

    time和datetime模块还有tzinfo (时区)(一直不明白两者的区别,然后摘了两片文章(最后面的两个链接),很清晰...) 一.time模块 常用函数 1. time()函数 time()函 ...

  9. 【查阅】教你使用SQL SERVER复制

    关键词:复制,复制总结,复制汇总,复制查阅 1.概念与搭建 Step1:SQL SERVER复制介绍 Step2:SQL Server 复制事务发布 Step3:SQL Server 通过备份文件初始 ...

  10. PHP自定义curl请求

    function http_post($url,$param,$post_file=false,$headers=[]){ $oCurl = curl_init(); if(stripos($url, ...