一、前言
本文通过一个Python socket代码,实现客户端与服务端之间的tcp通信,并简要分析Python中的socket API与linux中的socket API之间的关系。
二、通信原理
tcp连接的建立学过计算机网络课程的相信都有了解,三次握手的过程如下:
客户端发送连接请求,服务端接受请求,发回确认,客户端接受到后再次发送确认,即完成三次握手,建立了tcp连接,这与socket编程建立通信的过程何其相似,socket编程中客户端与服务端建立连接过程如下图:
可以看到建立过程分为几个步骤:
1、服务端调用socket()函数创建socket;
2、服务端调用bind()函数将ip和端口号与socket绑定;
3、服务端调用listen()函数启动侦听,监听来自客户端的连接请求;
4、客户端调用socket()函数创建socket;
5、客户端调用connect()函数,用服务端绑定时使用的相同ip和端口号建立连接;
6、服务端accept()函数响应,双方建立连接;
7、连接建立后,可以使用send()和recv()函数进行通信,发送和接受消息;
三、代码示例
#server.py import socket #创建套接字 sk=socket.socket() #创建一个元组类型,包含ip地址和端口号 ip_addr=("127.0.0.1",2222) #绑定socket和ip sk.bind(ip_addr) #启动侦听,队列长度为5 sk.listen(5) while True: print("prepare to connect") #调用accept()等待客户端连接,这里代码阻塞 conn,address=sk.accept() if conn: print("connect success") else: break while True: #接受数据,1024个字节从接受缓存取一次,收发不同步,收到exit断开本次连接 data=conn.recv(1024) receive=data.decode() if receive=="exit": print("current connect break") conn.close() break else: print(receive) #发送数据,遇到exit服务端主动断开 data=input("Please send something: ") if data=="exit": conn.send(data.encode()) conn.close() break else: conn.send(data.encode())
import socket sk=socket.socket() ip_addr=("127.0.0.1",2222) try: #连接服务端 sk.connect(ip_addr) print("connect success") #收发数据 while True: data=input("Please enter something: ") if data=="exit": print("connect break") sk.send(data.encode()) break else: sk.send(data.encode()) data=sk.recv(1024) receive=data.decode() if receive=="exit": print("connect break") break else: print(receive) except Exception as e: print("connenct unsuccess") sk.close()
可以看到服务端和客户端的实现过程与前面的图解过程完全一致,下面来看一下这些函数,看看它们都干了什么
3.1 socket()
调用socket()函数是为了创建套接字,语法为:socket.socket(socket_family,socket_type,protocol=0)
socket_family:即socket地址簇,即ip地址类型,通常情况使用AF_INET和AF_INET6,AF即为address family缩写;AF_INET表示ipv4地址,而
AF_INET6表示ipv6地址;
socket_type:规定数据传输类型,共有五种,但是我们常用的就两种,SOCK_STREAM和SOCK_DGRAM;其中SOCK_STREAM是指流式套接
字,也叫“面向连接的套接字”,建立可靠的tcp连接,上述代码使用的就是这种方式;而SOCK_DGRAM是数据报格式套接字,也叫“无连接的套接字”,
不建立可靠连接,不保证数据有序及无错误的到达;
protocol:表示传输协议,通常情况下计算机可以通过前两个参数自行推断出来,因此不赋值也可以;
上述代码中可以看到我们在创建套接字时,没有在socket()函数中附参数,这是因为Python在定义socket()函数时已经给定了初值,不给参数
则直接使用默认值,默认为使用ipv4地址,流格式;
3.2 socket()的各方法
有了socket对象后,就可以在这个基础上利用各种方法来实现服务端与客户端之间的通信;
socket.bind() 绑定地址到套接字,参数必须是元组,格式为(ip地址,端口号),例如:s.bind(('127.0.0.1',8009))
socket.listen(5) 开始监听,5为最大挂起的连接数,注意这里5是指挂起,而不是同时处理,服务端可以侦听到五个客户端的通信请求,但是
只能挨个处理;
socket.accept() 被动接受客户端连接,程序在这里阻塞,等待连接客户端套接字函数
socket.connect() 连接服务器端,参数必须是元组格式,与bind函数类似,例如:s.connect(('127,0.0.1',8009))
socket.recv(1024) 接收TCP数据,1024为一次数据接收的大小;这里提一下,tcp通信是“收发不同步”,这个不同步的意思是,服务端和客户
端都保持自己的节奏,在传输过程中有tcp控制着数据按序无误的到达目的地,但是数据到达接受缓存时,接收方并不是来了就拿,而是缓存满一次拿
一次,这个1024就是规定缓存数据达到多少取一次;
socket.send(bytes) 发送TCP数据,,这里要特别注意一下,python 3.x的版本,send的数据必须为bytes类型,因此发送前要encode()一
下,接收后要decode();
socket.sendall() 完整发送数据,内部循环调用send
socket.close() 关闭套接字
3.3 运行结果展示
服务端
客户端
四、Linux socket API 探究
编程语言中的socket模块的实现离不开操作系统底层的socket API的支持,在Linux系统,我们可以通过在终端输入命令man 2 socket,其余函数查看方法相同,查看Linux下socket的定义
socket - create an endpoint for communication
int socket(int domain, int type, int protocol);
socket是为了创建一个用于通信的端点,这里可以看见它的函数原型,基本和Python中的定义一样,所以可以确定Python中的socket函数就是对Linux中
的socket的函数的封装;
通过strace命令跟踪代码执行可以发现,Linux系统确实是一切皆文件,Python中的send()和recv()方法在Linux中是通过read和write实现的,更进
一步了解调用过程,可以大致得到以下对应关系:
由此我们知道Python以及其它编程语言实现socket模块离不开操作系统底层的socket模块,这其中是一一对应的关系;
五、改进想法
经过上述工作,相信动手的各位都能自己实现一个服务端与客户端通信的程序了,但是上面的程序还有一些不足之处,其中比较突出的是,这里的服
务端一次只能响应一个客户端的请求,为一个客户端提供服务,这与我们平常见到的QQ、微信之类的聊天程序是不一样的,这其中要用到多线程功能,服
务端为一个客户端开启一个线程,这样可以响应多个客户端请求,当然笔者受时间限制,未尽全功,以后有时间会继续深入研究socket编程的。
本文参考:https://github.com/mengning/net