上篇文章里我们讨论了大部分概念性的东西,接下来就一点点实现我们开头的那个例子吧。
前面我们说到,socket叫做套接字,是IP和port的结合。同时,我们也清楚地了解到通过ip和port,就可以在互联网中精准定位某一设备的某一进程。
下面来实现下C-S模式,就是Client和Server模式。服务器server等待客户端client连接,连接完毕后回传给client一些数据。
类比你现在的浏览器,把它当成client,你和csdn的server建立链接,链接成功后,server返回给了你信息,client拿到之后处理,就是你眼前的这个网页。
(后记:其实浏览器获得页面的模式准确的说是B-S,browser - server。我并不认为B-S独立于C-S模式,而是包含在C-S模式中,因为B-S模式里的客户端,就是浏览器嘛。B-S名称之所以出现,只是因为它用的多了,但它仍旧是一种C-S模式。包括另一种我们常见的P2P(peer-to-peer)点对点模式,也是一种特殊的C-S模式,只不过双方既是client又同时提供server服务罢了。我们的聊天软件最后也会做成这种模式。)
(后后记:按照工作方式分的话,互联网可分为边缘部分和核心部分两块。边缘部分是用户直接使用的,用于host间数据传递和共享。核心部分是给边缘部分提供服务的,由互联网中的路由器加网络组成,提供给边缘系统连通性。
边缘系统中的设备我们一般称作主机,也就是host。边缘系统中host的通信方式,就是我们上面说到的那几种模式了,c-s模式,p2p模式。
计算机间的通信,严格来说应该是运行在两个host主机上的进程之间的通信,而不应该简单地理解成计算机之间的通信。也因此,在这里我们有必要来探讨client和server指代的是什么东西。在我们的这个例子中,显然代表着进程。client是一个进程,server是一个进程,我们将两个进程同时运行在本地,就成了一台host上的两个进程之间的通信。如果client程序和server程序没有运行在同一host上,就成了一般情况下我们说的client和server了模式了。
运行client程序的主机,也叫做client,不过我们翻译的时候喜欢翻译成客户机或者客户端。而运行server程序的主机我们叫做服务器或者叫做服务器端。我们在生活中说的:“嘿,哥们,我能在你买的服务器上放个网页吗?”。这里的服务器显然不是server进程,而是运行server进程的host。也因此,在我们判断client和server是代表硬件还是软件的时候,需要从上下文中理解并加以分析啦。)
我们用到的socket库,是标准库,不需要另外安装。包括后面的可视化,我们也是直接用的tkinter。(我这里是Python3)
先来看服务器端server怎么写:
#import module
import socket
#create object
sk = socket.socket()
#maybe? waiting test
ip_port = ('192.168.1.117', 55555)
#bind listen
sk.bind(ip_port)
#the max number of connection
sk.listen(5)
#waring message
print('Running')
#get data
conn, address = sk.accept()
print(address)
#def message
msg = '你好啊'
#send message default byte transform, encode if transform string
conn.send(msg.encode())
#close connection
conn.close()
超简单,没几行代码。跳过导入模块和建立socket对象不说。定义变量ip_port,我们说过了,通过IP和port就可以精确定位到一台PC上的一个进程。这个IP就是我们的PC的IP,这个进程,我们占用55555端口号。之后通过bind绑定ip和port进行监听。listen设置了最大连接数。
配置消息完毕后,通过socket对象的accept方法建立链接,返回数据我们用conn和address两个变量解包接收,conn连接对象等会用于数据回传,address我们直接输出看看发送请求的地址是什么,来证明我们的猜想。
后面定义了回传msg变量,通过conn对象的send方法回传数据。数据传输是字节流形式,因此如果发送中文,记得encode编码后再发送,client那边接受到后decode解码再输出就可以。
最后链接关闭,close。
上面的过程完美的模拟了你的浏览器和服务器建立连接后,服务器给你回传数据的过程。除了回传的数据不一样外。
有两点注意:
1.我们前面说过,这是基于TCP(传输控制协议)协议实现的。两端需要先建立链接,再发送数据。而我们最终的聊天工具是通过UDP(用户数据协议)协议实现的,后面我们说它们的区别。
2.如果是在本机做测试,ip_port变量中的ip可以写成127.0.0.1,这个ip指的就是本地ip。
server构建完毕,来看client:
import socket
client = socket.socket()
#bind ip and port
ip_port = ('192.168.1.117', 55555)
#connect server
client.connect(ip_port)
#get server data
data = client.recv(1024)
#print data, decode if data is string
print(data.decode())
更简单,socket对象创建,ip_port变量配置成我们服务器程序的ip和port。然后我们就可以,通过socket对象的connect直接连接服务器。
连接建立后,recv方法接收数据,这里就会收到我们server发回来的‘你好啊’字符串。参数是接收数据的字节数,将数据decode输出来查看。
来运行下,先启动server:
我们编写的server进程就占用了我们本机的55555端口运行,程序输出了running,然后挂起。Why?
因为server在等待client连接啊,连接完了才会往下走。
我们用IDLE启动了server,重新打开个IDLE运行client也行,或者用cmd启动client启动也行。不要用同一个IDLE,没办法同时运行两个程序。
client发起连接,recv接收到了server的消息,程序退出。
回看server:
输出了client的ip和port,首先两个进程都是我们本机上的,ip肯定是一样的了,但是client的端口却是57809。
这就告诉你了,向你发送请求的是192.168.1.117这台主机上,端口号是57809的进程。
现在你就可以玩了,在你的本机上搭建个server服务器,然后启动它。用你哥们的PC(局域网ip需要在同一个网络中,我们前面有说)编写client,运行去链接你PC上的ip_port,看看能不能在你哥们的PC上获得你PC上的数据吧。
成功了吗?
成功了的话,我们再深入点?
现在的c-s模型能完成的功能远不是我们想要的,我们的聊天工具,需要一直发送数据,另一边需要一直接收数据并输出。不能发送一次,链接就断开了。
来,进阶,server:
import socket
sk = socket.socket()
#maybe? waiting test
ip_port = ('127.0.0.1', 55555)
#bind listen
sk.bind(ip_port)
#the max number of connection
sk.listen(5)
#waring message
print('Running')
#get data
conn, address = sk.accept()
while True:
print(conn.recv(1024).decode())
#close connection
conn.close()
我们将server改进下,让server不断地接受client发送的数据并进行输出。
逻辑很简单,把recv写进while里,不断循环接收。
client的改进更加简单:
import socket
sk = socket.socket()
ip_port = ('127.0.0.1', 55555)
sk.connect(ip_port)
while True:
send = input('What :')
sk.send(send.encode())
把send写进循环里,不断发送消息。
运行server:
输出running,挂起,等待接收。
运行client:
等待输入。
我们发送点东西:
看看server那边怎么样?
完全没问题,接收到了。
ok,现在你又可以继续玩了,在你的PC上重新编写server,一直等待接收数据。用你哥们的PC编写client,一直发送数据,看看你的服务器起不起作用吧。记得ip要用你自己的ip啊。
对于两个进程间的数据交换有了更进一步的认识了吗?现在我们在逻辑上分析我们需要实现的功能,聊天需要一直接收对面发来的消息,同时还要一边给对面发送我们的数据。
也就是说,我们的工具要同时具有server和client的功能,既能接收,又能发送。
我们知道在recv时,程序会挂起,同样的在input时,程序也会挂起。问题来了,怎样让recv接收的那个循环和input发送的那个循环不要互相影响呢?
哎!多线程!(子进程也可以解决,但是启动一个线程的开销要比启动一个进程小的多,何况我们实现的这个东西又及其简单。)
让两个循环跑在不同的线程里面,就不会影响了。
这篇文章先到这里,多线程这个问题也先留在这里。下篇我们说UDP协议,对比TCP协议,我们就能很明显的感觉到,UDP对于我们这个聊天工具的实现更契合。