前言
一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)
如果你要跟别人一起玩,那你就需要上网了,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。
传输模型
传输协议
互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层
每层运行常见物理设备
每层的作用
具体应用
客户端/服务器架构
1.硬件C/S架构(打印机)
2.软件C/S架构(互联网中处处是C/S架构,比如QQ):B/S架构也是C/S架构的一种,B/S是浏览器/服务器
3. C/S架构的开发主要运用socket。
socket通信
socket层
socket概念
socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
TCP协议和UDP协议
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
TCP三次握手和四次挥手
建立TCP需要三次握手才能建立,而断开连接则需要四次握手。整个过程如下图所示:
三次握手
首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。
“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。举例:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天balabala……”
两次握手:
“喂,你听得到吗?”
“我听得到呀”
“喂喂,你听得到吗?”
“草,我听得到呀!!!!”
“你TM能不能听到我讲话啊!!喂!”
“……”
四次挥手
假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
Python socket通信
family(socket家族) socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成 socket.AF_INET:(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个, python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET) socket type类型 socket.SOCK_STREAM #for tcp socket.SOCK_DGRAM #for udp socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。 socket.SOCK_SEQPACKET #废弃了
服务端套接字函数
- s.bind() 绑定(主机,端口号)到套接字
- s.listen() 开始TCP监听
- s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
- s.connect() 主动初始化TCP服务器连接
- s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
- s.recv() 接收数据
- s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
- s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
- s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
- s.getpeername() 连接到当前套接字的远端的地址
- s.close() 关闭套接字
- socket.setblocking(flag) #True or False,设置socket为非阻塞模式,以后讲io异步时会用
- socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo('luffycity.com',80)
- socket.getfqdn() 拿到本机的主机名
- socket.gethostbyname() 通过域名解析ip地址
基于TCP协议的socket
服务器端
import socket sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 开启socket sock.bind(('127.0.0.1', 8898)) # 绑定IP和端口到套接字 sock.listen(5) # 监听,5表示客户端最大连接数 conn, addr = sock.accept() # 被动接受TCP客户的连接,等待连接的到来 ret = conn.recv(1024) # 接收客户端信息 print(ret) # 打印客户端信息 conn.send(b'hi') # 向客户端发送信息byte数据 conn.close() # 关闭客户端套接字 sock.close() # 关闭服务器套接字(可选)
客户端
import socket client = socket.socket() # 创建客户套接字 client.connect(('127.0.0.1',8898)) # 尝试连接服务器 client.send(b'hello!') # 发送消息 ret = client.recv(1024) # 对话(发送/接收) print(ret) client.close() # 关闭客户套接字
在连接过程中有时会出现以下问题,这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址
解决
import socket # 加入一条socket配置,重用ip和端口 from socket import SOL_SOCKET,SO_REUSEADDR sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 开启socket sock.setsockopt(SOL_SOCKET,SO_REUSEADDR, 1) # 就是它,在bind前加 sock.bind(('127.0.0.1', 8898)) # 绑定IP和端口到套接字 sock.listen(5) # 监听,5表示客户端最大连接数 conn, addr = sock.accept() # 被动接受TCP客户的连接,等待连接的到来
案例:基于TCP协议模拟ssh远程消息传递
服务器端
import socket CONN_ADDR = ('127.0.0.1', 9999) sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 开启socket sock.bind(CONN_ADDR) # 绑定IP和端口到套接字 sock.listen(5) # 监听,5表示客户端最大连接数 print('start listen') while True: conn, addr = sock.accept() # 被动接受TCP客户的连接,等待连接的到来 print('connect by ', addr) while True: data = conn.recv(1024) # 接收数据1024字节 if data: print('收到的数据是{}'.format(data.decode())) conn.send(data) else: print('close conn') conn.close() break sock.close() print('close sever socket') break
客户端
import socket client = socket.socket() client.connect(('127.0.0.1', 9999)) while True: msg = input(">>>") if msg != 'q': client.send(msg.encode()) data = client.recv(1024) print('收到的数据{}'.format(data.decode())) else: client.close() print('close client socket') break
输出结果
服务器:
客户端:
问题:一个服务器端只能同时服务一个客户端,因为accept和recv都会阻塞。