24 网络编程-TCP

在网络中,由IP地址可以唯一确定一台主机。但真正实现网络通信时并不是主机间一对一的通信,而是运行在各个主机上的应用程序(进程或线程)多对多的相互交换信息和数据。 网络通信的各个主机上可能运行着很多的程序,为了标识出、实现多(台主机)对多(个程序间)通信,需要标识出某台主机上的某个应用程序和另一台主机的某个应用程序间通信关系,在网络通信时需标地除了某主机的IP地址外,还需要端口号来唯一确定主机中的各个通讯程序正在和另外一ip地址的主机的某应用程序使用的端口通信。这IP地址加端口号的方式就构成了网络通信过程中的唯一标识符,即套接字socket 。 通过套接字(ip + port)可以实现多主机多应用程序间同时通信,极大地提高了网络的应用能力。最简单的例子是一台主机上可以开n个qq可以和另一台主机的m个qq聊天,尽管只有这两个人。

如何理解端口和ip?

端口实际上代表的是一个网络通信的应用程序,以手机通信为例,每台手机可以接收短信、接电话、上微信,一条数据到达了某手机,是在短信里显示还是微信里显示?手机依据什么来区分到达本手机的数据是该微信收还是该短信收呢?端口号!到达的数据里含有端口信息,分析出了端口信息就知道数据是给谁的,这就是端口的意义所在,而在天上飞来飞去的数据怎么到达想去的手机呢?数据里含有手机号信息,等价于主机的ip地址。 常用的Socket 类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket (SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP 服务应用;数据报式Socket 是一种无连接的Socket,对应于无连接的UDP 服务应用。

24.1 TCP和UDP

TCP传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP 连接的请求。

UDP用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。\par 本章主要围绕TCP展开,展示Python下如何通过TCP实现网络通信。

24.2 TCP网络编程

使用socket模块进行tcp编程一般需要写服务器和客户端两个程序,tcp的服务器端程序和客户端程序都该干些什么呢? tcp的服务器端和客户端程序,有点像拨打服务热线一样,首先说服务热线端该做那些工作,服务热线首先要对外公布热线号码等待(监听)客户打电话咨询。服务热线怎样同时接听多个客户同时打入电话咨询呢?服务热线内部设置多位接听电话的接线员,客户打入电话后实际上是(分派)在跟分机的接线员在交流。一位客户向某接线员咨询完毕后结束本次通信,所有客户都咨询完毕了,热线是不能关闭的,对于tcp通信的服务程序实际也是不能关闭的,所以一般是个无限循环,但总有可能想最终结束,例如公司倒闭热线自然就没了。 从这个例子可以看出,实际有两个线路实现了客户咨询的业务,一路是客户打入热线,一路是热线和接线员,有了这两路才真正的实现客户咨询的。在tcp里也有这样的情况,后续解释。 接着聊tcp的客户端要做的事情,还是热线为例,客户需要有个手机,得知道热线的电话号码,然后拨电话建立电话通信,如果热线接线员有限且都在咨询,客户需要等,接线员有空闲则可以和某空闲的接线员咨询业务,咨询完毕后可以挂电话了。

24.2.1 tcp服务器端程序

通过热线的例子口水话的介绍了tcp服务器端和客户端所要做的一些事情,下面回到Python世界。Python编程实现Tcp服务器程序需要以下几步:

1).创建套接字

创建套接字,等价于公司准备开设热线,是电话?微信公众号?还是网络?所以得指定热线实现形式,在Python里需要用socket模块里的socket函数创建实现。

from socket import *
sockobj = socket(AF_INET, SOCK_STREAM)

作用:买个程控交换机,准备热线设备。socket函数里的AF_INET是说采用ipv4的tcp或udp通信机制,SOCK_STREAM代表tcp通信。这sockobj是tcp服务器端第一个套接字。

2).绑定ip和端口

通信设备买好了,该把设备用起来了,对外公布热线吧,在python的网络通信里需要绑定ip和端口,等价于公司建立热线电话号码。这是python的tcp服务器程序需要绑定ip和端口,使用socket模块里的bind函数绑定本地地址和端口号,这个套接字又称服务套接字。

sockobj.bind((ip地址, port端口))

3).监听

设备买了,也向电信申请了热线号码,上电等电话吧。Python的tcp服务器需要调用socket模块里的listen函数来等待客户端连接,那热线有几个接线员呢?liesten的参数可以设置服务器可同时接收多少个客户端同时在线。

sockobj.listen(max_client)

4).接受连接

每有一个咨询电话打入就转接给一个接线员,让他们俩聊吧,热线腾出来可以接入其他客户咨询电话。在Python里调用accept函数接入每次呼入的电话并创建内部线路使得某一接线员和咨询客户一对一的交流。所以accept函数的返回值有两个,一个代表某接线员和某咨询电话通路,一个是打入的咨询电话信息。从套接字的角度看accept返回的接线员和客户电话通路也是一个套接字,而第二个返回值即咨询电话的信息是客户端的ip和端口。

connection, address = sockobj.accept( )

connection是热线内某接线员和某咨询客户一对一通信(请求)套接字。这是tcp通信服务器端程序里的第二个套接字。listen设置最大接听数,(计算机)系统会监视和本服务程序的连接的个数,当连接个数大于listen设置值,系统会直接拒绝新的连接请求,而listen设置之内的若干个连接客户端是不会被拒绝的,需要注意的是不被拒绝未必能直接和服务器能实时通信,这取决于热线话务员(接线员)的是否到岗是否上班,通常一个服务器端程序在计算机里是一个进程,如果有多个客户端同时上线要和服务器程序进行多对多的通信,需要服务器端软件创建若干个线程,由进程创建的线程和每一个客户端进行一对一的通信,交换数据。 如果服务器端的程序没有创建线程,那无论有多少个客户端程序向服务器端发起请求连接,也仅有一个客户端被服务,因为服务器端就一个进程,不可能一下对付那么多的通信请求。

5).收发数据

当服务器端通过accept函数接受了某客户端的连接(客户端会用connect函数发出请求连接)请求,保留此客户端的信息在accept的第一个返回值(地址和端口信息也就是第二个套接字,又称请求套接字)里。通过这个返回值调用send或者recv函数向或者从连接的客户端发生或接收数据。

connection.send(数据)
data = connection.recv(数量)

6).关闭请求套接字

当某客户和服务器间数据传递完毕后,就可以关闭请求套接字了connection,相当于热线电话的客户挂断了电话,结束咨询。

connection.close()

7).关闭服务套接字

当整个服务器无客户端请求或关闭系统前需要关闭服务套接字sockobj,相当于公司倒闭服务热线取消。

sockobj.close()

24.2.2 Tcp客户端程序

对应于Tcp的服务器程序的主要步骤,Tcp的客户端也要做一些事情,才能和Tcp服务器端建立面向连接的通信信道。步骤和服务器端相差的不多。

1).创建套接字

这是显然的,客户要和某公司取得联系,可以同够短信、微信、邮件、电话等多种形式,既然选择的打咨询热线电话,那么得知道首先得有一个电话,还得知道热线的电话是多少啊!所以在客户端也需要创建一个套接字,指定和服务器端的交流方式,打电话!

sockobj = socket(AF_INET, SOCK_STREAM)

和服务器端一样就不解释了。

2).发出请求连接

客户端不接受别人的请求,只请求服务器,所以不需要调用bind来绑定自己的ip和端口开放接收外部服务请求。所以bind可以省了、listen也省了。 客户端可以调用connect向服务器发起连接请求,等价于客户拨了热线,能不能通还不知道。

sockobj.connection()

如果服务器端忙正服务于某客户端程序,客户端程序会阻塞在connect函数,即connect未执行完毕,直到服务器端空闲可以和本客户端程序建立连接,connect函数才算执行完毕。

3).收发数据

客户端通过recv和send和服务器端交互数据。

sockobj.recv(数量)
sockobj.send(数据)

4).关闭套接字

数据交换完毕后可以关闭客户端程序的套接字了,相当于挂电话。 以上就是Tcp客户端程序所要做的事情,比服务器端少了bind、listen、accpt几个步骤,也不创建第二个套接字,只有一个套接字。

24.3 Tcp服务端和客户端程序示例

本节先做一个简单的Tcp网络通信程序示例,用于帮助大家理解Tcp网络通信的架构和基本步骤,在这个简单的一对一的Tcp程序之后,给大家展示一对多的Tcp网络通信程序,最后介绍SocketServer模块也是实现多对多的Tcp网络通信。

24.3.1 简单的一对一Tcp通信程序

下面给出一个简单的一对一通信的Tcp服务器端和客户端示例程序。

服务器端程序s.py

#coding:utf-8
from socket import *
#''代表服务器为 localhost
myHost = ''
#在一个非保留端口号上进行监听
myPort = 50007
#设置一个TCP socket对象
sockobj = socket(AF_INET, SOCK_STREAM)
#绑定端口号
sockobj.bind((myHost, myPort))
#监听,允许5个连结
sockobj.listen(5)
#直到进程结束时才结束循环
while True:
    #等待客户端连接
    connection, address = sockobj.accept( )
    #连接是一个新的socket
    print 'Server connected by', address
    while True:
        #读取客户端套接字的下一行
        data = connection.recv(1024)
        #如果没有数量的话,那么跳出循环
        if not data: break
        #发送一个回复至客户端
        connection.send('Echo from server => ' + data)
    #当socket关闭时eof
    connection.close( )

客户端程序c.py

#coding:utf-8
import sys
from socket import *
serverHost = 'localhost'
serverPort = 50007
#发送至服务端的默认文本
message = ['Hello network world']
#建立一个tcp/ip套接字对象
sockobj = socket(AF_INET, SOCK_STREAM)
#连接至服务器及端口
sockobj.connect((serverHost, serverPort))
for line in message:
    #经过套按字发送line至服务端
    sockobj.send(line)
    #从服务端接收到的数据,上限为1k
    data = sockobj.recv(1024)
    print 'Client received:', repr(data)
#关闭套接字
sockobj.close( )

测试

本Tcp网络通信程序,在Linux计算机下需要开多个终端Terminal,在第一个终端里执行python s.py,在其他终端里执行python c.py。windows计算机打开多个cmd即可,其中一个执行python s.py。运行的s.py 和多个c.py程序,会发现运行s.py的终端里只有和多个c.py的其中一个进行数据通信,其余的c.py里无任何反应。

原因是服务器就一个进程,换句话说就一部电话。只能服务一个客户端,当按ctrl c 结束正使用服务的c.py时,另一个c.py被s.py相应了。

这个问题好像有悖于网络服务的本质,怎能一对一呢?如何解决?可以每次有客户端请求时服务端创建一个线程专门服务于这个客户请求,即热线有接线员了。

24.3.2 多对多的Tcp网络通信

基本思想是每次有客户端请求服务的时候,服务器端的程序就创建一个新的线程专门服务于该客户端的服务请求。 较之于s.py程序,下面的server.py代码有使用了threading模块来创建线程的内容。

服务器端server.py

#coding:utf-8

from socket import *
import threading
# 服务器和客户端数据通信
def stoc(client_socket, addr):
    while True:
        try:
            client_socket.settimeout(500)
            buf = client_socket.recv(1024)
            print "server got msg from", str(addr[1]), buf
            client_socket.send(buf + str(addr[1]))
        except socket.timeout:
            print 'time out'
            break
    client_socket.close()
myHost = ''
myPort = 50025
sockobj = socket(AF_INET, SOCK_STREAM)
sockobj.bind((myHost, myPort))
sockobj.listen(5)
while True:
    # 接受某客户端服务请求
    client, address = sockobj.accept( )
    print 'Server connected by', address
    # 为该客户请求建立专有服务线程
    thread = threading.Thread(target=stoc, args=(client, address))
    # 启动线程,激活
    thread.start()

客户端client.py

#coding:utf-8
import sys,time
from socket import *
serverHost = 'localhost'
serverPort = 50025
sockobj = socket(AF_INET, SOCK_STREAM)
sockobj.connect((serverHost, serverPort))
while True:
    line = "cpython"
    sockobj.send(line)
    time.sleep(4)
    data = sockobj.recv(1024)
    if not data: break
    print 'Client received:', data
sockobj.close( )

本程序和上面的c.py没太大区别。

测试

也开四个终端Terminal,在其中一个运行server.py其余三个运行client.py。

从截图可以看出,每个客户端client.py都能和server.py交换数据。使用netstat -al | grep "50025"可以查看出与端口50025连接的tcp网络状态信息,如下图所示,也能看出客户端的端口号和其余几个Terminal里打印出的信息是一致的。

24.4 SocketServer模块

上一节是用的socket模块、threading模块实现的Tcp网络通信多对对的通信服务。本节使用Python里自带的另一个模块SocketServer来实现多对多的Tcp通信。更多可以参考SocketServer 在线内容。

服务器端程序ss.py

服务器端程序里使用了SocketServer模块。ThreadingTCPServer实现的socket服务器内部会为每个client创建一个“线程”,该线程用来heels客户端进行交互。专门处理客户端请求的自定义MyServer类继承于SocketServer里的BaseRequestHandler类,需重载其handle()方法,self.request代表已建立的某客户端请求,即服务请求套接字。

#!/usr/bin/env python
#coding:utf-8
import SocketServer
import subprocess

class MyServer(SocketServer.BaseRequestHandler):
    def handle(self):
        while True:
            print self.request.getsockname()
            conn = self.request
            data = conn.recv(1024)
            print "sever got from ",self.client_address, data
            ack_msg = "got from "+ str(ip) + " to " + str(self.client_address) + data
            conn.send(ack_msg)

if __name__ == '__main__':
    server = SocketServer.ThreadingTCPServer(('localhost',9527), MyServer)
    ip, port = server.server_address
    print ip, port
    server.serve_forever()

客户端程序cc.py

客户端依然使用socket模块,通过connect函数向服务器发起连接请求,和之前的两个客户端程序c.py、client.py没太大差别。

#!/usr/bin/env python
#coding:utf-8
import socket
ip_port = ('localhost',9527)
sk = socket.socket()
sk.connect(ip_port)
while True:
    send_msg = "hello"
    sk.send(send_msg)
    print "client send", send_msg
    recv_msg = sk.recv(100)
    print "client recv", recv_msg
sk.close()

测试

可以开四个终端Terminal。一个运行ss.py,另外三个运行cc.py。从下面的截图可以看出,每个客户端程序cc.py都能和服务端程序ss.py进行数据通信。