什么是TCP协议
TCP( Transmission control protocol )即传输控制协议,是一种面向连接、可靠的数据传输协议,它是为了在不可靠的互联网上提供可靠的端到端字节流而专门设计的一个传输协议。
-
面向连接 :数据传输之前客户端和服务器端必须建立连接
-
可靠的 :数据传输是有序的 要对数据进行校验
TCP三次握手
为了保证客户端和服务器端的可靠连接,TCP建立连接时必须要进行三次会话,也叫TCP三次握手,进行三次握手的目的是为了确认双方的接收能力和发送能力是否正常。
- 第一次握手(呼叫请求):
- 你(客户端):想要和某人通话,于是你拿起电话,拨打对方的号码。这个动作相当于TCP中的SYN(同步序列编号)包发送。你告诉对方(服务器):“嘿,我在这里,我想和你建立通话。”
- 第二次握手(接听确认):
- 对方(服务器):听到电话铃声后,接起电话,并对你的呼叫做出响应。这相当于TCP中的SYN-ACK(同步确认)包发送。对方(服务器)说:“我听到了,我也在这里,我们可以开始通话了。”同时,对方(服务器)也确认了你的存在,并准备好接下来的通信。
- 第三次握手(确认接听):
- 你(客户端):在听到对方的接听确认后,你回应一个确认信号,告诉对方你已经准备好开始通话了。这相当于TCP中的ACK(确认)包发送。你说:“好的,我已经准备好了,我们可以开始通话了。”
- 第一次握手 TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT 同步已发送状态
- 第二次握手 TCP服务器收到请求报文后,如果同意连接,则会向客户端发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了 SYN-RCVD 同步收到状态.
- 第三次握手 TCP客户端收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED已建立连接状态 触发三次握手
三次握手主要作用:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误
第一次握手: 客户端向服务器端发送报文 证明客户端的发送能力正常 第二次握手:服务器端接收到报文并向客户端发送报文 证明服务器端的接收能力、发送能力正常 第三次握手:客户端向服务器发送报文 证明客户端的接收能力正常
TCP四次挥手
建立TCP连接需要三次握手,终止TCP连接需要四次挥手。
张三:好的,那我先走了
李四:好的,那你走吧
李四:那我也走了?
张三:好的,你走吧
Socket的定义
Socket的英文原义是“孔”或“插座”,网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket),socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。
Python 官方关于 Socket 的函数 socket — Low-level networking interface — Python 3.12.4 documentation
Socket的类型
-
TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的, 每次发送要指定是发给谁。
-
服务端与客户端不能直接发送列表,元组,字典。需要字节化(data)。
服务端socket函数:
客户端Socket函数
共有的函数 :
SOCKET函数 | 描述 |
---|---|
s.recv(bufsize[,flag]) | 接受TCP,UDP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。 |
s.send(string[,flag]) | 发送TCP,UDP数据。 |
s.sendall(string[,flag]) | 完整发送TCP数据。 |
s.recvfrom(bufsize[.flag]) | 接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。 |
s.sendto(string[,flag],address) | 发送UDP数据。将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。 |
s.close() | 关闭套接字。 |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
s.getsockname() | 返回套接字自己的地址。 |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
s.getsockopt(level,optname[.buflen]) | 设置给定套接字选项的值。 |
s.settimeout(timeout) | 设置套接字操作的超时期 |
s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。 |
s.makefile() | 创建一个与该套接字相关连的文件 |
TCP实现一对一的简单聊天
服务端代码:
from socket import *
tcp_server = socket(AF_INET, SOCK_STREAM)
# 绑定本地信息,TCP服务器程序监听所有网络接口上的88888端口
host_port = ('', 88888)
tcp_server.bind(host_port)
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcp_server.listen(5)
while True:
newSocket, host_port = tcp_server.accept()
while True:
# 最大接收1024个字节
recvData = newSocket.recv(1024)
if len(recvData) > 0:
print('recv:', recvData)
else:
break
sendData = input("send:")
newSocket.send(sendData.encode('utf8'))
newSocket.close()
tcp_server.close()
客户端代码:
from socket import *
tcp_client = socket(AF_INET, SOCK_STREAM)
# 链接服务器
host_port = ('127.0.0.1', 88888)
tcp_client.connect(host_port)
while True:
sendData = input("send:")
if len(sendData) > 0:
tcp_client.send(sendData.encode('utf8'))
else:
break
# 最大接收1024个字节
recvData = tcp_client.recv(1024)
print('recv:', recvData.decode('uft8'))
tcp_client.close()
UDP
UDP ---(User Datagram Protocol) 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,没有超时重发等机制,所以传输速度很快。
UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
UDP特点:
UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。 UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。 UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。
【适用情况】 UDP是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP一般用于多点通信和实时的数据业务,比如
-
语音广播
-
视频
-
QQ
-
TFTP(简单文件传送)
-
大型网络游戏
相比较于TCP注重速度流畅
UDP简单案例
服务端代码
import socket
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定 IP 和端口号
sk.bind(('127.0.0.1', 88888))
while True:
msg, addr = sk.recvfrom(1024)
print('来自[%s:%s]的消息: %s' % (addr[0], addr[1], msg.decode('utf-8')))
inp = input('>>>')
sk.sendto(inp.encode('utf-8'), addr)
sk.close()
客户端代码:
import socket
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
addr = ('127.0.0.1', 88888)
while True:
msg = input('>>>')
sk.sendto(msg.encode('utf-8'), addr)
msg_recv, addr = sk.recvfrom(1024)
print(msg_recv.decode('utf-8'))
sk.close()
实现简单TFTP(基于UDP协议)
实现一个简单的TFTP(Trivial File Transfer Protocol)服务器和客户端基于UDP协议是一个有趣的小练习。TFTP是一个简单的文件传输协议,它使用UDP协议,并且通常用于小文件的传输。
我们值实现最基本的功能:从服务器读取文件并发送到客户端。
TFTP 服务器:
import socket
import struct
def handle_client(sock, filename):
# 假设文件存在且小于65535字节
try:
with open(filename, 'rb') as file:
data = file.read(512) # TFTP块大小为512字节
block_number = 1
while data:
# 发送DATA包:block_number + data
block_info = struct.pack('!H', block_number) # 网络字节序的短整型
sock.sendto(block_info + data, client_address)
# 等待ACK
ack_data, _ = sock.recvfrom(2) # 只接收block number
ack_block_number = struct.unpack('!H', ack_data)[0]
if ack_block_number != block_number:
print("Incorrect block number in ACK")
break
block_number += 1
data = file.read(512)
except FileNotFoundError:
# 发送ERROR包
error_code = 1 # File not found
error_msg = struct.pack('!HH', error_code, 0) # TFTP错误消息格式
sock.sendto(error_msg, client_address)
def start_tftp_server(port, root_dir):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('0.0.0.0', port))
print(f"TFTP server listening on port {port}...")
while True:
data, client_address = server_socket.recvfrom(1024) # 接收RRQ或WRQ请求
# 这里简化处理,只处理RRQ
if data.startswith(b'\x00\x01'): # RRQ的opcode
filename = data[2:].decode().split(b'\x00')[0]
filepath = f"{root_dir}/{filename}"
handle_client(server_socket, filepath)
if __name__ == "__main__":
start_tftp_server(69, './files')
TFTP 客户端:
import socket
import struct
def request_file(server_ip, server_port, filename):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送RRQ请求
rrq = b'\x00\x01' + filename.encode() + b'\x00' + b'octet' + b'\x00' # mode为octet
client_socket.sendto(rrq, (server_ip, server_port))
block_number = 0
data = b''
while True:
# 接收DATA包
data_packet, _ = client_socket.recvfrom(514) # 512字节数据 + 2字节块号
block_number = struct.unpack('!H', data_packet[:2])[0]
if block_number == 0: # 假设块号为0表示文件结束或错误
break
data += data_packet[2:]
# 发送ACK
ack = struct.pack('!H', block_number)
client_socket.sendto(ack, (server_ip, server_port))
client_socket.close()
# 处理接收到的数据(例如,写入文件)
with open(filename, 'wb') as file:
file.write(data)
if __name__ == "__main__":
request_file('127.0.0.1', 69)
file_name = 'example.txt'
request_file(server_addr, 69, file_name)