概述
本文通过一个最简单的Socket通信来对每一步做通俗易懂的讲解让你了解这些函数到底是干什么用的。下面的代码虽然是用Pyhton实现的,但是你要知道这些通信机制并不是Python所定义的,因为这些东西都必须符合通用规范。在类Unix操作系统上建立Socket也是这些步骤而Python其实就是使用的系统调用。同时本文包括关于Socket的基础知识,这些知识对于你去理解Socket很有帮助。
你的第一个Socket程序
基础知识
套接字:标识每个端点的IP和端口就叫做一个套接字。比如:(192.168.50.100:80)
套接字对儿:包含两个端点(本机和对端)的组合,也就是本地IP和端口以及远程主机IP和端口。比如:(192.168.50.112:59091 192.168.50.100:80)
监听套接字:socket函数产生一个尚未打开的主动套接字,bind把该套接字具体到一个地址,该套接字只有本地IP+端口。服务器在调用listen函数之后那么该套接字就变成一个监听套接字。告诉内核该套接字可以接收连接请求。
连接套接字:如果有客户端连接进来那么就会为每一个连接过来的客户端产生一个连接套接字(本地IP、PORT 远程IP、port)。
套接字函数:套接字函数使用描述符访问套接字,一个套接字可以对应多个描述符,但是一个描述符只能属于一个套接字。套接字就是应用层到传输层或其他协议层的访问接口。而访问任意套接字就要用到描述符,因为通过描述符才能调用套接字函数。
Socket发送缓冲区:每一个TCP套接字有一个发送缓冲区,这个缓冲区你可以更改大小。当应用程序调用wirte的时候,内核从应用进程的缓冲区复制所有数据到套接字发送缓冲区,如果应用程序缓冲区中的数据大于套接字发送缓冲区(可能本身就大于,有可能套接字发送缓冲区本来就有数据剩余的空间小于要发送的数据)这时候应用进程将会被设置为睡眠也就是内核将不从write函数返回,应用进程就卡在这里了。卡在这里干嘛呢?其实就是等待,等要发送的数据全部复制到套接字发送缓冲区之后才返回,不过这里虽然返回了也不代表已经把数据发送到远程主机了,这种返回仅仅代表告诉应用程序你可以重新使用应用程序缓冲区并且往里面写数据。套接字在内核空间,应用程序在用户空间,write就是把数据从用户空间复制到内核空间。复制完了之后的真实发送数据以及TCP的数据可靠机制这些东西都是有TCP协议栈来保证的无需上层应用进程来关系。有人就问套接字发送缓冲区感觉多余啊,理想状态下的确多余,但是有2个必要好处:
- TCP是可靠连接,它需要保证你要发送的数据确实发送成功,TCP把缓冲区的数据发送到对端,它还要等对端的ACK确认,之后确认之后它才会把缓冲区的数据删除。
- 解耦,应用进程把数据丢进来就好了,剩下的工作我来在,你去干其他的事情。
在UDP中虽然也有一个套接字发送缓冲区,但是其作用仅仅是标识这个UDP数据包的大小上限,它不会去做保证可靠的事情,所以它的缓冲区也不会保存应用进程要发送的数据。
服务器端
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 4 # 导入 socket 模块 5 import socket 6 7 """ 8 创建 socket 套接字对象,如果成功返回非负数的描述符,如果失败返回-1,严格来说这里的套接字是一个主动套接字。而且是一个未连接 9 (CLOSED状态)的主动套接字。 10 family 指定协议族 11 AF_INET IPv4协议 12 AF_INET5 IPv6协议 13 AF_LOCAL Unix套接字协议 14 AF_ROUTE 路由套接字 15 AF_KEY 密钥套接字 16 type 指定套接字类型 17 SOCK_STREAM 字节流套接字 TCP 18 SOCK_DGRAM 数据报套接字 UDP 19 SOCK_SEQPACKET 有序分组套接字 20 SOCK_RAW 原始套接字 21 proto 设置某个协议的值,默认是0, 0表示根据给定family和type的组合自动设置当前系统默认值 22 IPPROTO_TCP TCP传输协议 23 IPPROTO_UDP UDP传输协议 24 IPPROTO_SCTP SCTP传输协议 25 如果socket()什么都不填写则表示IPv4的TCP协议。上面这些参数的值不是python语言里socket函数所独有的,而是系统调用函数具有的。 26 """ 27 s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP) # 等于 s = socket.socket() 28 29 """ 30 该函数会把一个声明了类型的套接字具体化。也就是你上面声明了使用什么网络层协议和传输层协议,如果把服务器和客户端通信比作写信, 31 你要给我写信你就得遵守我的规范比如一次写多少字因为太多了我看不过来,用什么样的信封等等的一些要求,规范订完了那别人的知道 32 怎么找到服务器啊也就是你的邮寄地址,因为规范谁都可以用,N个人可以使用同一个规范,但是如何区别N个人呢?这就是IP和端口。 33 所以bind就是声明一个地址并且是符合上面定义的规范的地址。其实IP和端口本身毫无意义,192.168.1.100:80 这样的地址之所以可以 34 表示一个符合IP和TCP的规范的地址是因为在IP和TCP协议中对地址格式进行了定义。 35 36 套接字实在socket()函数调用时就创建了,只是这时候是一个没有明确地址的套接字,它是套接字主体,这里的IP和端口是绑定在套接字 37 上的客体。 38 39 绑定的时候需要指定具体IP和端口吗?其实不用,你可以把IP和端口写成空你看看程序能运行么?当然可以,这里的可以是说它可以运行 40 并监听在某个IP和端口上,至于是哪个IP和端口取决于当前系统的设置。但作为服务器来说通常需要指定IP和端口,在RPC通讯中服务器 41 绑定不需要指定端口,这是例外。 42 如果不指定IP和端口我怎么知道服务器监听在哪里呢?这时候你就需要使用 getsockname()函数来获取。 43 """ 44 s.bind(("127.0.0.1", 12345)) 45 46 """ 47 listen(backlog)函数把一个未连接的主动套接字变成一个被动套接字,也就是告诉内核接受连接到该套接字的请求。这时候套接字状态将从CLOSED 48 变成LISTEN状态。这个函数的参数是一个整数其含义是套接字队列的最大连接个数。 49 内核为套接字准备2个队列: 50 一个是半连接队列也就是服务器收到客户端SYN并回复SYN_ACK之后等待三次握手完成的队列,套接字状态(SYN_RCVD) 51 一个是全连接队列也就是三次握手建立完毕之后的队列,完成之后客户端连接就会从半连接队列移动到全连接队列的末尾,套接字状态(ESTABLISHED) 52 上面两个队列到底指的是这2个队列中的哪一个还是他俩之和又或者说是两者中的最大值这个不一定具体得看具体操作系统在这个系统调用 53 上的定义。 54 在红帽Linux内核里backlog指的是全连接队列大小 net.core.somaxconn;而半连接队列大小有另外一个值 tcp_max_syn_backlog。那么这个 55 backlog有默认值但是应用程序可以修改。 56 """ 57 s.listen(5) 58 59 while True: 60 """ 61 获取一个客户端连接,这个函数其实就是从全连接队列的队首返回一个已经完成三次握手的连接,如果这个全连接队列为空则进程挂起 62 也就是睡眠直到该队列有一个可用连接被返回。该函数调用成功将会返回一个连接套接字和协议地址。 63 """ 64 c, addr = s.accept() 65 print("连接套接字:", c) 66 print('连接地址:', addr) 67 data = "Hello world!" 68 # 发送数据 69 c.send(data.encode(encoding="utf-8")) 70 71 """ 72 我们通常使用close()函数来关闭一个套接字连接,其实它并不是真正的关闭,它只是让这个连接套接字的引用计数器减1,只有当这个 73 某个套接字的引用计数值为0时,在会被真正的清理和释放资源。换句话说调用clese函数不会直接出发四次断开机制,也就是服务器 74 不会主动发送FIN。如果你真的要主动发送FIN就要使用shutdown()函数。不过通常我们都使用close。 75 """ 76 c.close()
客户端
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 4 import socket 5 6 """ 7 这里和服务器端的设置是一样的,创建一个主动未打开的套接字。 8 """ 9 s = socket.socket() 10 11 """ 12 这里有些人可能看不懂,网上内多套接字编程客户端根本不用bind()函数啊。没错的确不用,但是你不写并不代表内核不用,如果你不写 13 内核会帮你找一个当前系统使用的IP并随机产生一个端口。如果你写一个固定的也没错。如果你看了前面服务端对bind函数的说明你就 14 知道为什么一定要显式或隐式的调用bind()函数了。客户端程序不明确调用这个函数只是因为在C/S模式中作为客户端的一方不需要被别人 15 主动连接过来所以它bind可以使用本机任意可以和外面通信的IP以及一个随机端口。 16 """ 17 s.bind(("127.0.0.1", 9999)) 18 19 """ 20 connect()函数是用来与服务器端建立TCP连接使用的,成功返回0否则返回-1。调用该函数会触发TCP的三次握手。当收到SYN_ACK时该函数返回。 21 """ 22 s.connect(("127.0.0.1", 12345)) 23 print("使用:", s.getsockname(), " 连接远程服务器。") 24 # 接收数据 25 data = s.recv(1024) 26 print(data.decode(encoding="utf-8")) 27 s.close()
运行结果
这时候是阻塞的模式一次只能允许一个客户端连接过来。