传输控制协议(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:

四、理性分析时间

  1. sock.bind((interface, port)) 决定了远程主机是否能尝试连接服务器,以及服务器是否不接受外部连接而只与本机上的其他运行程序通信。
  2. 命令行里host如果填空的话,表示可以通过机器的任意IP地址接受连接请求。
  3. 客户端程序:首先创建了一个套接字——然后以想要通信的服务器地址作为参数运行connect()——接着就能随意的发送或接收数据了。
  4. UDP发送和接收都是一整个数据报,而TCP会在传输过程中把数据流分为多个大小不同的数据包,然后在接收端将这些数据包逐步重组。
  5. 为什么尽量使用sendall(),少用send()呢?
    进行TCP send()时,因为TCP是基于流的协议,所以数据传输和接收取决于套接字缓冲区的空余,会出现被阻塞、部分发送等的情况。而sendall()不仅运行快且在所有数据发送完之前不会竞争资源。
  6. 但是没有recvall(),所以recv()要在循环中调用。
  7. 套接字操作:
    (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 实现远程骰宝游戏相关推荐

  1. 【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

    参考连接:https://www.nowcoder.com/study/live/504/2/16. [Linux]网络编程一:网络结构模式.MAC/IP/端口.网络模型.协议及网络通信过程简单介绍 ...

  2. Python之网络编程(基于tcp实现远程执行命令)

    文章目录 实现目标 服务端分析 客户端分析 远程执行结果 本篇是用tcp套接字实现的一个远程执行命令的小案例,tcp套接字是一种面向连接的Socekt,针对面向连接的TCP服务应用,安全,但是效率低 ...

  3. 22.1 网络编程:软件结构、网络通信协议、UDP与TCP协议、(网络编程三要素:协议、ip地址、端口号)、查看ip地址、检测网络是否连通、判断ip是否可用

    目录 网络编程 软件结构 网络通信协议 TCP/IP协议 协议分类 UDP TCP 网络编程三要素 协议 IP地址 列:查看本机ip地址 检查网络是否连通.判断ip是否可用 端口号 网络编程 软件结构 ...

  4. Java:网络编程,网络编程三要素,TCP协议,UDP协议

    day23 网络编程 网络编程三要素: IP地址 端口号 通信协议 TCP协议 UDP协议 网络编程 1.网络:计算机网络,由在不同地理位置.不同的计算机主机,互联形成的一个计算机系统.有通讯和数据共 ...

  5. TCP/IP网络编程之基于TCP的服务端/客户端(一)

    TCP/IP网络编程之基于TCP的服务端/客户端(一) 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于 ...

  6. 您所应了解的Python四大主流网络编程框架

    本文内容摘录自<Python高效开发实战--Django.Tornado.Flask.Twisted>一书.该书分为三部分:第1部分是基础篇,带领初学者实践Python开发环境和掌握基本语 ...

  7. Python学习笔记:网络编程

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  8. Linux网络编程---详解TCP

    Linux网络编程---详解TCP的三次握手和四次挥手_shanghx_123的博客-CSDN博客_tcp的协议数据单元被称为 TCP协议详解(TCP报文.三次握手.四次挥手.TIME_WAIT状态. ...

  9. Java——网络编程三要素

    * A:计算机网络* 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统.网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统.* ...

最新文章

  1. 客户端dmesg_打印调试技术 printk klogd dmesg(解决打印信息的问题)
  2. python pandas空值与缺失值处理
  3. 小白一路走来,连续刷题三年,谈谈我的算法学习经验
  4. 编写自己的Shell解释器-3[转]
  5. Composer -- PHP依赖管理的用法
  6. map与unordered_map
  7. 华为手机harmonyos系统,华为王成录:手机销量仍在增长 未来会是HarmonyOS系统的中心...
  8. Nagios配置监控windows客户端
  9. 一个类中可以没有main方法_一个月可以暴瘦二十斤的减肥方法
  10. 【Java】---JVM内存模型
  11. 计算机名称位数怎么改,请问下怎样更改电脑位数
  12. Android Camera2 Opengles2.0 实时滤镜(冷暖色/放大镜/模糊/美颜)
  13. 计算机配置很不错但是卡,高手告诉你win10电脑明明配置很好却卡顿的详尽处理手法...
  14. js实现视频直播,结合bilibili开源项目
  15. 项目经理做什么工作的,每个公司不一样吗?
  16. 蚂蚁花呗账单分期和交易分期的费用如何计算?
  17. 怎么设置能在IIS6内设置显示错误信息?
  18. Python 英文分词
  19. linux忘了用户名和密码_Linux 服务器忘记用户名密码的找回办法总结linux操作系统 -电脑资料...
  20. 如何解决电脑不停自动下载安装软件问题?

热门文章

  1. Rserver部分配置
  2. FCKeditor 2.0在线编辑器的设置与修改以及使用
  3. Python与简单网络爬虫的编写
  4. 再回首往事如梦,再回首。。。。。。
  5. 塞尔希奥·阿奎罗和 The Sandbox 携手合作,激活元宇宙足球迷!
  6. 网页文本编辑器插入网页中无法正常显示运行
  7. 你听过最美的网名是什么?
  8. sublime简要笔记
  9. matplotlib plot python rgb2gry 显示灰度图像
  10. 华为荣耀magic是鸿蒙系统,荣耀Magic3是什么系统-采用什么系统