Python C/S 网络编程(三)之 TCP 实现远程骰宝游戏
传输控制协议(TCP/IP)是互联网的重要组成部分。
互联网由一整套的协议组成,TCP只是其中的一层。
最底层的以太网协议(Ethernet)规定了电子信号如何组成数据包(packet),解决了子网内部的点对点通信。但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。
IP 协议定义了一套自己的地址规则,称为 IP 地址。它实现了路由功能,允许某个局域网的 A 主机,向另一个局域网的 B 主机发送消息。
IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。
简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。传输文档和文件的协议几乎都是使用TCP的。包括通过浏览器浏览网页、文件传输以及用与电子邮件传输的所有主要机制。
一、TCP工作原理
我们都知道UDP是不可靠的协议,所以UDP传输可能会出现诸如丢包、冗余、乱序等错误;而TCP传输,数据包就被隐藏在协议层之下了。应用程序只需要向目标机器发送流数据,TCP会将丢失的信息重传,保证信息能够成功到达目的机器。
TCP传输基本原理:
- 每个TCP数据包都有一个序列号,接收方通过该序列号将响应数据包正确排序。也可以通过该序列号发现传输序列中丢失的数据包,并请求重传。
- TCP并不使用顺序的整数(1,2,3…)作为数据包的序列号,而是通过一个计数器来记录发送的字节数。例如,如果一个包含1024字节的数据包的序列号为7200,那么下一个数据包的序列号就是8224。
- 在一个优秀的TCP实现中,初始序列号是随机选择的。
- TCP并不通过锁步的方式进行通信。相反,TCP无须等待响应就能一口气发送多个数据包。我们把在某一时刻发送方希望同时传输的数据量成为TCP窗口大小。
- 接收方的TCP实现可以通过控制发送方的窗口大小来减缓或暂停,叫做流量控制。
- 最后,若TCP认为数据包被丢弃了,他会假定网络正在变得拥挤,然后减少每秒发送的数据量。
- 注意一点:TCP四次挥手?其实也可以只发三次:FIN、FIN-ACK、ACK,这样会比较快速,四次就是每个方向上都发一对FIN、ACK。
二、TCP套接字
和UDP一样,TCP也使用端口号来区分同一IP地址上运行的不同应用程序。
上一节有说道,UDP其实也是可以调用connect()函数的,但是和TCP还是有些区别:
- TCP中调用connect会引起三次握手,client与server建立连结。
UDP中调用connect内核仅仅在OS内部把对端ip&port记录下来。 - UDP中可以多次调用 connect,TCP只能调用一次 connect。
- TCP的connect调用是有可能失败的。远程主机可能不作出应答,也可能拒绝连接,协议错误…原因就是TCP刘涉及两台主机间持续连接的建立。另一方的主机需要处于正在监听的状态,并做好接收连接请求的准备。
- TCP的S(服务器)端不进行connect()调用,而是接收C端connect()调用的初始SYN数据包。且在Python中S端在接受连接请求的同时还新建了一个套接字。
TCP接口实际上包含了两种套接字类型:“被动”监听套接字和主动“连接”套接字。
(1)被动套接字(监听套接字):服务器通过该套接字来接受连接请求,但不能用于发送或接收任何数据,也不表示任何实际的网络会话。而是由服务器指示被动套接字通知OS首先使用哪个特定的TCP端口号来接受连接请求。
由IP地址和正在监听的端口号唯一标识,因此任何其他程序都不能再使用相同的IP地址和端口。
(2)主动套接字(连接套接字):将一个特定的IP地址和端口号和某个与其进行远程会话的主机绑定,只用于与该特定主机进行通信,可以通过该套接字发送或接收数据。
唯一标识主动套接字的是一个四元组:
(local_ip, local_port, remote_ip, remote_port)
且多个主动套接字可以共享一个本地套接字名(IP&port) 。
三、一个简单的C/S程序
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter03/tcp_sixteen.py
# Simple TCP client and server that send and receive 16 octetsimport argparse, socket#保证一个循环内接收定长数据
def recvall(sock, length):data = b''while len(data) < length:more = sock.recv(length - len(data))if not more:raise EOFError('was expecting %d bytes but only received'' %d bytes before the socket closed'% (length, len(data)))data += morereturn datadef server(interface, port):sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建套接字sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #设置套接字选项sock.bind((interface, port)) #绑定本地接口sock.listen(1) #监听print('Listening at', sock.getsockname())while True:print('Waiting to accept a new connection')sc, sockname = sock.accept() #接受一个客户端的连接请求并返回一个新的套接字print('We have accepted a connection from', sockname) #所连接的套接字名print(' Socket name:', sc.getsockname()) #自己的套接字名print(' Socket peer:', sc.getpeername()) #所连接的套接字名message = recvall(sc, 16) #接收16字节的数据print(' Incoming sixteen-octet message:', repr(message)) #将消息转为字符串sc.sendall(b'Farewell, client') #发送客户端sc.close() #关闭套接字print(' Reply sent, socket closed')def client(host, port):sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建套接字sock.connect((host, port)) #请求连接print('Client has been assigned socket name', sock.getsockname())sock.sendall(b'Hi there, server')reply = recvall(sock, 16)print('The server said', repr(reply))sock.close()if __name__ == '__main__':choices = {'client': client, 'server': server}parser = argparse.ArgumentParser(description='Send and receive over TCP')parser.add_argument('role', choices=choices, help='which role to play')parser.add_argument('host', help='interface the server listens at;'' host the client sends to')parser.add_argument('-p', metavar='PORT', type=int, default=1060,help='TCP port (default 1060)')args = parser.parse_args()function = choices[args.role]function(args.host, args.p)
关于argparse库的用法见:
https://blog.csdn.net/weixin_41206209/article/details/84643746
运行结果:
S:
C:
四、理性分析时间
- sock.bind((interface, port)) 决定了远程主机是否能尝试连接服务器,以及服务器是否不接受外部连接而只与本机上的其他运行程序通信。
- 命令行里host如果填空的话,表示可以通过机器的任意IP地址接受连接请求。
- 客户端程序:首先创建了一个套接字——然后以想要通信的服务器地址作为参数运行connect()——接着就能随意的发送或接收数据了。
- UDP发送和接收都是一整个数据报,而TCP会在传输过程中把数据流分为多个大小不同的数据包,然后在接收端将这些数据包逐步重组。
- 为什么尽量使用sendall(),少用send()呢?
进行TCP send()时,因为TCP是基于流的协议,所以数据传输和接收取决于套接字缓冲区的空余,会出现被阻塞、部分发送等的情况。而sendall()不仅运行快且在所有数据发送完之前不会竞争资源。 - 但是没有recvall(),所以recv()要在循环中调用。
- 套接字操作:
(1)S端通过运行bind()来声明一个特定的端口,该端口可以只用于某一特定IP接口,也可以用于所有借口。注意:此时还没决定该程序会被作为S端还是C端。
(2)下一个调用是listen()。程序通过该调用表明,它希望套接字进行监听,此时才真正决定了程序要作为S。
(3)该套接字现在只能通过它的accept()方法来接受连接请求。(accept()方法唯一目的就是用于支持TCP套接字的监听功能。)每次调用accept()方法都会等待一个新的C连接S,然后返回一个新的套接字。
(4)一旦C和S完成了所有需要的通信,他们就会调用close()方法关闭套接字,通知OS将输出缓冲区中剩余的数据传输完成,然后使用FIN数据包的关闭流程来结束TCP会话进程。
五、地址被占用
为什么要在绑定地址前设置套接字选项呢?
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #设置套接字选项
有时候关闭S端后再重启时会发现报错:OSError:[Errno 98] Address already in use
因为在TCP关闭套接字后,OS的网络栈一般不会立即关闭而经历TIME_WAIT的过程
六、死锁
如果通信双方都写数据,套接字缓冲区被越来越多的数据填满,而这些数据从未被读取,就容易造成死锁。最终在某个方向上再也无法通过send()发送数据,就可能会永远等待缓冲区清空,从而导致阻塞。
避免死锁的两种方式:
1、客户端和服务器可以通过套接字选项将阻塞关闭。
2、程序可以使用某种技术同时处理来自多个输入的数据。可以采用多个进程或线程来处理。
七、已关闭连接,半开连接
半关:套接字方法shutdown()可以用来为套接字生成一个方向上的文件结束符,同时保持另一方向的连接处于打开状态。
八、像使用文件一样使用TCP流
如果想要把一个套接字传递给一个支持读取和写入普通文件对象的Python模块:可以使用makefile()方法,该方法返回一个Python对象。
九、实战——远程骰宝小游戏设计
1.简要说下题目:
交互式游戏设计(RemoteBet) (基于TCP):
通过在远端主机上搭建一个远程骰宝服务器,其它主机可以通过客户端程序
RemoteBet与远程的骰宝服务器联系,进行交互式游戏设计。
2.程序设计思路:
- tcp远程骰宝游戏,即客户端与服务器端通信,由服务器端上的骰子筛出点数再传递给客户端。
- 首先,要完成基本的基于TCP的C/S通信模型。服务器端写一个筛骰子的函数,当客户端连接上时,记录其端口信息。每个循环筛四个筛子,因为TCP是基于流的传输协议,可以将筛好的点数统一放在一个对象中传输给客户端。客户端在接收时控制每次取数的窗口大小即可。
- 其次,客户端接收到服务器端发来的点数后,通过在字典中查找对应的符号输出来实现筛子的图样,再分别编写表示结果和金库的函数即可。
3.源码:
S:
from socket import *
import random,sysHOST = '127.0.0.1'
ADDR = ('127.0.0.1',8880)
tcpSer = socket(AF_INET,SOCK_STREAM) #创建套接字:tcp是基于流的传输协议tcpSer.bind(ADDR) #绑定地址(端口号)
tcpSer.listen(1000) #设置监听(最大连接数)while True:print('RemoteBet {}'.format(HOST))#等待客户端的连接#accept()函数会返回一个元组#元素1为客户端的socket对象,元素2为客户端的地址(ip地址,端口号)cli, add = tcpSer.accept()print('{} 加入了游戏'.format(add)) #记录玩家信息while True:def roll_dice(): #筛骰子函数n = random.randint(1,6)return str(n)for m in range(1,5): #循环筛4次m = roll_dice()cli.send(m.encode('utf-8')) #每次筛好都发送到tcp传输队列中print(m)data = cli.recv(1024).decode('utf-8') #接受客户数据并解码if not data or data == 'exit':sys.exit()print('玩家{}说:'.format(add)+data) #玩家结果cli.close()
tcpSer.close() #关闭套接字
C:
from socket import *
import sysHOST = '127.0.0.1'
PORT = 8880
ADDR = (HOST,PORT)
coin = 1
gold = 100 * coin
silver = 10 * coin
money = 100cliSocket = socket(AF_INET,SOCK_STREAM)
cliSocket.connect(ADDR) #建立连接print('-------------------------------------------')
print('欢迎来到风月赌场,规则如下:')
rules = """ya tc <数量> <coin|silver|gold> 押头彩(两数顺序及点数均正确) 一赔三十五ya dc <数量> <coin|silver|gold> 押大彩(两数点数正确) 一赔十七ya kp <数量> <coin|silver|gold> 押空盘(两数不同且均为偶数) 一赔五ya qx <数量> <coin|silver|gold> 押七星(两数之和为七) 一赔五ya dd <数量> <coin|silver|gold> 押单对(两数均为奇数) 一赔三ya sx <数量> <coin|silver|gold> 押散星(两数之和为三、五、九、十一) 一赔二"""
print(rules)#骰子图样字典
dict1 = {'1':''' ┌───┐│ ││ ● ││ │└───┘''','2':'''┌───┐│ ● ││ ││ ● │└───┘''','3':'''┌───┐│ ● ││ ││● ●│└───┘''','4':'''┌───┐│● ●││ ││● ●│└───┘''','5':'''┌───┐│● ●││ ● ││● ●│└───┘''','6':'''┌───┐│● ●││● ●││● ●│└───┘'''}
dict2 = {'1':'一','2':'二','3':'三','4':'四','5':'五','6':'六'} #骰子点数字典
dict3 = {'tc':'头彩','dc':'','kp':'空盘','qx':'七星','dd':'单对','sx':'散星'} #结果字典def oe(s): #判断奇偶if (s%2) == 0:return 0else:return 1def results(): #该局结果global n1,n2,n3,n4,yan1 = int(n1)n2 = int(n2)n3 = int(n3)n4 = int(n4)if (n1==n3) and (n2==n4):return 'tc'elif ((n1==n3)and(n2==n4))or((n1==n4)and(n2==n3)):return 'dc'elif (n3!=n4) and (oe(n3)==0) and (oe(n4)==0):return 'kp'elif ((n3+n4)==7):return 'qx'elif (oe(n3)==1) and (oe(n4)==1):return 'dd'elif ((n3+n4)==3)or((n3+n4)==5)or((n3+n4)==9)or((n3+n4)==11):return 'sx'else:return '没有该种点型!'sys.exit()def winorlose(): #金库状况global moneyif ya['ctype'] == 'gold':ym = int(ya['number']) * 100elif ya['ctype'] == 'silver':ym =int(ya['number']) * 10elif ya['ctype'] == 'coin':ym = int(ya['number'])else:print('error')sys.exit()if ya['wtype'] == results():print('恭喜你,你赢了')if ya['wtype'] == 'tc':money += ym * 35elif ya['wtype'] == 'dc':money += ym * 17elif ya['wtype'] == 'kp':money += ym * 5elif ya['wtype'] == 'qx':money += ym * 5elif ya['wtype'] == 'dd':money += ym * 3elif ya['wtype'] == 'sx':money += ym * 2else:print('你输了呢!')if ya['wtype'] == 'tc':money -= ym * 35elif ya['wtype'] == 'dc':money -= ym * 17elif ya['wtype'] == 'kp':money -= ym * 5elif ya['wtype'] == 'qx':money -= ym * 5elif ya['wtype'] == 'dd':money -= ym * 3elif ya['wtype'] == 'sx':money -= ym * 2print('目前金库 : {} coin'.format(money))#循环是为了保证能持续进行通话
while True:print("""庄家唱道:新开盘!预叫头彩!庄家将两枚玉骰往银盘中一撒。""")global n1,n2,n3,n4,yan1 = (cliSocket.recv(1).decode('utf-8')) # 1字节接受服务器端发来的点数n2 = (cliSocket.recv(1).decode('utf-8'))print(dict1[n1],dict1[n2]) #输出预叫头彩print('庄家唱道:头彩骰号是 {}、{}'.format(dict2[n1],dict2[n2]))ya0 = input('输入你押的值 (ya <玩法> <数量> <coin|silver|gold>) :')if ya0 == 'exit': #退出cliSocket.send(ya0.encode('utf-8'))sys.exit()else: #将玩家输入的信息切割为字典形式存放ya2 = ya0.split(' ')ya1 = ['ya','wtype','number','ctype']ya = dict(zip(ya1,ya2))cliSocket.send(ya0.encode('utf-8')) #将玩家下的赌注发送给服务器端print("""庄家将两枚玉骰扔进两个金盅,一手持一盅摇将起来。庄家将左手的金盅倒扣在银盘上,玉骰滚了出来。""")n3 = (cliSocket.recv(1).decode('utf-8'))print(dict1[n3])print("""庄家将右手的金盅倒扣在银盘上,玉骰滚了出来。""")n4 = (cliSocket.recv(1).decode('utf-8'))print(dict1[n4])print('庄家叫道:{}、{}'.format(dict2[n3],dict2[n4]))print('.......{}'.format(dict3[results()])) #结果winorlose() #金库情况cliSocket.send(str(results()).encode('utf-8')) #以字节流的形式发送数据,需编码
cliSocket.close() #关闭客户端
4.测试结果:
C:
S:
5.错误环节:
当然我又遇到了很多问题…
对于字典掌握不熟,后来查了知道对于玩家输入的信息:ya tc 10 gold可以先用split()分成列表,在创建一个关于“key”的列表:ya1 = [‘ya’, ‘wtype’, ‘number’, ‘ctype’] ,两个列表用zip缝上就变成了一个字典: {‘ya’: ‘ya’, ‘wtype’: ‘tc’, ‘number’: ‘10’, ‘ctype’: ‘gold’}
关于金币的换算:
一开始是这样写的
发现不管输入什么金币,还是按照coin 的币种来算:
后面终于发现错:赢或者输时,不应该是用金库来倍加倍减,而应该用玩家押的币种来算,再存入金库:
当然这个小程序很简陋,可以加入try,exception这些异常处理,可以做成类(看起来更好),我赶着交作业写的比较随意(就是懒…)。
Python C/S 网络编程(三)之 TCP 实现远程骰宝游戏相关推荐
- 【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写
参考连接:https://www.nowcoder.com/study/live/504/2/16. [Linux]网络编程一:网络结构模式.MAC/IP/端口.网络模型.协议及网络通信过程简单介绍 ...
- Python之网络编程(基于tcp实现远程执行命令)
文章目录 实现目标 服务端分析 客户端分析 远程执行结果 本篇是用tcp套接字实现的一个远程执行命令的小案例,tcp套接字是一种面向连接的Socekt,针对面向连接的TCP服务应用,安全,但是效率低 ...
- 22.1 网络编程:软件结构、网络通信协议、UDP与TCP协议、(网络编程三要素:协议、ip地址、端口号)、查看ip地址、检测网络是否连通、判断ip是否可用
目录 网络编程 软件结构 网络通信协议 TCP/IP协议 协议分类 UDP TCP 网络编程三要素 协议 IP地址 列:查看本机ip地址 检查网络是否连通.判断ip是否可用 端口号 网络编程 软件结构 ...
- Java:网络编程,网络编程三要素,TCP协议,UDP协议
day23 网络编程 网络编程三要素: IP地址 端口号 通信协议 TCP协议 UDP协议 网络编程 1.网络:计算机网络,由在不同地理位置.不同的计算机主机,互联形成的一个计算机系统.有通讯和数据共 ...
- TCP/IP网络编程之基于TCP的服务端/客户端(一)
TCP/IP网络编程之基于TCP的服务端/客户端(一) 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于 ...
- 您所应了解的Python四大主流网络编程框架
本文内容摘录自<Python高效开发实战--Django.Tornado.Flask.Twisted>一书.该书分为三部分:第1部分是基础篇,带领初学者实践Python开发环境和掌握基本语 ...
- Python学习笔记:网络编程
前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...
- Linux网络编程---详解TCP
Linux网络编程---详解TCP的三次握手和四次挥手_shanghx_123的博客-CSDN博客_tcp的协议数据单元被称为 TCP协议详解(TCP报文.三次握手.四次挥手.TIME_WAIT状态. ...
- Java——网络编程三要素
* A:计算机网络* 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统.网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统.* ...
最新文章
- 客户端dmesg_打印调试技术 printk klogd dmesg(解决打印信息的问题)
- python pandas空值与缺失值处理
- 小白一路走来,连续刷题三年,谈谈我的算法学习经验
- 编写自己的Shell解释器-3[转]
- Composer -- PHP依赖管理的用法
- map与unordered_map
- 华为手机harmonyos系统,华为王成录:手机销量仍在增长 未来会是HarmonyOS系统的中心...
- Nagios配置监控windows客户端
- 一个类中可以没有main方法_一个月可以暴瘦二十斤的减肥方法
- 【Java】---JVM内存模型
- 计算机名称位数怎么改,请问下怎样更改电脑位数
- Android Camera2 Opengles2.0 实时滤镜(冷暖色/放大镜/模糊/美颜)
- 计算机配置很不错但是卡,高手告诉你win10电脑明明配置很好却卡顿的详尽处理手法...
- js实现视频直播,结合bilibili开源项目
- 项目经理做什么工作的,每个公司不一样吗?
- 蚂蚁花呗账单分期和交易分期的费用如何计算?
- 怎么设置能在IIS6内设置显示错误信息?
- Python 英文分词
- linux忘了用户名和密码_Linux 服务器忘记用户名密码的找回办法总结linux操作系统 -电脑资料...
- 如何解决电脑不停自动下载安装软件问题?