Skip to content

11. Python 的网络函数

现在我们来探讨 Python 的网络函数,这些函数使我们能够进行 TCP / IP(传输控制协议 / 互联网协议)编程。

  

11.1. 获取互联网上某台机器的名称或地址


程序 (inet_01)


import sys, socket

#------------------------------------------------
def getIPandName(nomMachine):
    #nomMachine:要获取其地址的机器名称 IP
    # nomMachine-->地址 IP
    try:
        ip=socket.gethostbyname(nomMachine)
        print "ip[%s]=%s" % (nomMachine,ip)
    except socket.error, erreur:
        print "ip[%s]=%s" % (nomMachine,erreur)
        return

    # 地址 IP --> nomMachine
    try:
        name=socket.gethostbyaddr(ip)
        print "name[%s]=%s" % (ip,name[0])
    except socket.error, erreur:
        print "name[%s]=%s" % (ip,erreur)
        return


# ---------------------------------------- 主

# 常量
HOTES=["istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","localhost","","xx"]

# HOTES 机器的 IP 地址
for i in range(len(HOTES)):
    getIPandName(HOTES[i])
# 结束
sys.exit()

注:

  • 第 1 行:Python 的网络函数封装在 socket 模块中。

结果

1
2
3
4
5
6
7
8
9
ip[istia.univ-angers.fr]=193.49.146.171
name[193.49.146.171]=istia.istia.univ-angers.fr
ip[www.univ-angers.fr]=193.49.144.40
name[193.49.144.40]=ametys-fo.univ-angers.fr
ip[www.ibm.com]=129.42.58.216
name[129.42.58.216]=[Errno 11004] host not found
ip[localhost]=127.0.0.1
name[127.0.0.1]=Gportpers3.ad.univ-angers.fr
ip[xx]=[Errno 11004] getaddrinfo failed

11.2. 一个 Web 客户端

一个用于获取网站索引段落内容的脚本。


程序 (inet_02)


import sys,socket

  #-----------------------------------------------------------------------
def getIndex(site):
    # 读取网站 URL 并将其存储在文件 site.html 中
  
    # 起初没有错误
    erreur=""
    # 创建文件 site.html
    try:
        html=open("%s.html" % (site),"w")
    except IOError, erreur:
        pass
    # 错误?
    if erreur:
        return "Erreur (%s) lors de la création du fichier %s.html" % (erreur,site)

    # 在网站80端口建立连接
    try:
        connexion=socket.create_connection((site,80))
    except socket.error, erreur:
        pass
    # 若出错则返回
    if erreur:
        return "Echec de la connexion au site (%s,80) : %s" % (site,erreur)

    # 连接代表双向通信流
    # 在客户端(本程序)与所连接的Web服务器之间
    # 该通道用于交换命令和信息
    # 对话协议为 HTTP

    # 客户端发送 get 命令以请求 URL /
    # GET语法 URL HTTP/1.0
    # HTTP 协议的头部(headers)必须以空行结尾
    connexion.send("GET / HTTP/1.0\n\n")

    # 服务器现在将在连接通道上进行响应。它将发送所有
    # 数据,然后关闭通道。客户端会读取连接通道中接收到的所有数据
    # 直至通道关闭
    ligne=connexion.recv(1000)
    while(ligne):
        html.write(ligne)
        ligne=connexion.recv(1000)

    # 客户端随后关闭连接
    connexion.close()
    # 关闭 HTML 文件
    html.close()
    # 返回
    return "Transfert reussi du paragraphe index du site %s" % (site)

# --------------------------------------------- main    

# 从 URL 获取文本 HTML

# 网站列表
SITES=("istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","xx")

# 读取表格中网站的索引页面 SITES
for i in range(len(SITES)):
      # 读取网站 SITES 的索引页 [i]
      resultat=getIndex(SITES[i])
      # 显示结果
      print resultat

# 结束
sys.exit()

注:

  • 第 57 行:要获取首页的网站 URL 列表。该列表存储在文本文件 [nomsite.html] 中;
  • 第62行:由函数getIndex执行该操作;
  • 第4行:函数 getIndex;
  • 第20行:方法 create_connection((site,port)) 用于与在机器 site 的端口 port 上运行的 TCP / IP 服务建立连接
  • 第35行:方法 send 允许通过 TCP / IP 连接发送数据。 此处发送的是文本。该文本遵循 HTTP 协议(HyperText 传输协议);
  • 第40行:recv方法可通过TCP / IP连接接收数据。 在此,Web 服务器的响应以 1000 个字符为单位读取,并存储在文本文件 [nomsite.html] 中。

结果

Transfert reussi du paragraphe index du site istia.univ-angers.fr
Transfert reussi du paragraphe index du site www.univ-angers.fr
Transfert reussi du paragraphe index du site www.ibm.com
Echec de la connexion au site (xx,80) : [Errno 11001] getaddrinfo failed

网站 [www.ibm.com] 收到的文件:

HTTP/1.1 302 Found
Date: Wed, 08 Jun 2011 15:43:56 GMT
Server: IBM_HTTP_Server
Content-Type: text/html
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache, must-revalidate
Location: http://www.ibm.com/us/en/
Content-Length: 209
Kp-eeAlive: timeout=10, max=14
Connection: Keep-Alive

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/en/">here</a>.</p>
</body></html>
  • 第1-11行是服务器响应中的HTTP头部信息;
  • 第1行:服务器要求客户端重定向至第8行指定的URL;
  • 第2行:响应的日期和时间;
  • 第3行:Web服务器的标识;
  • 第4行:服务器发送的内容。此处为第13行开始的HTML页面;
  • 第12行:结束HTTP标头的空行;
  • 第13-19行:Web服务器发送的HTML页面。

11.3. 一个 SMTP 客户端

在 TCP / IP 协议中,SMTP(SendMail 传输协议)是消息发送服务的通信协议。


程序 (inet_03)


# -*- coding=utf-8 -*-

import sys,socket

#-----------------------------------------------------------------------
def getInfos(fichier):
    # 返回从文本文件中提取的信息(smtp、发件人、收件人、消息)[fichier]
    # 第1行:SMTP、发件人、收件人
    # 后续行:邮件正文

    # 打开 [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # 读取第一行
    ligne=infos.readline()
    # 删除换行符
    ligne=cutNewLineChar(ligne)
    # 提取 smtp、发件人、收件人字段
    champs=ligne.split(",")
    # 字段数量是否正确?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # 对检索到的信息进行“处理”
    # 从这3个字段中分别删除有用信息前后多余的“空格”
    for i in range(3):
        champs[i]=champs[i].strip()
    # 提取字段
    (smtpServer,expediteur,destinataire)=champs
    message=""
    # 读取消息剩余部分
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # 返回
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # 代表发件人将消息发送至 SMTP 服务器 smtpserver
    # 发送给收件人。如果 verbose=True,则记录客户端与服务器的交互

    # 获取客户端名称
    try:
        client=socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
    except socket.error, erreur:
        return "Erreur IP / Nom du client : %s" % (erreur)
    
    # 在 smtpServer 的 25 端口上建立连接
    try:
        connexion=socket.create_connection((smtpServer,25))
    except socket.error, erreur:
        return "Echec de la connexion au site (%s,25) : %s" % (smtpServer,erreur)

    # 连接代表客户端(本程序)与所联系的 SMTP 服务器之间的双向通信流
    # 在客户端(本程序)与所连接的SMTP服务器之间
    # 该通道用于交换命令和信息

    # 连接建立后,服务器会发送一条欢迎消息,我们读取该消息
    erreur=sendCommand(connexion,"",verbose,1)
    if(erreur) :
        connexion.close()
        return erreur
    # ehlo命令:
    erreur=sendCommand(connexion,"EHLO %s" % (client),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # 命令 mail from:
    erreur=sendCommand(connexion,"MAIL FROM: <%s>" % (expediteur),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # 命令 rcpt to:
    erreur=sendCommand(connexion,"RCPT TO: <%s>" % (destinataire),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # 数据命令
    erreur=sendCommand(connexion,"DATA",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # 准备发送消息
    # 它必须包含以下行
    # 发件人:发件人
    # 收件人:
    # 空行
    # 消息
    # .
    data="From: %s\r\nTo: %s\r\n%s\r\n.\r\n" % (expediteur,destinataire,message)
    # 发送消息
    erreur=sendCommand(connexion,data,verbose,0)
    if erreur :
        connexion.close()
        return erreur
    # 退出命令
    erreur=sendCommand(connexion,"QUIT",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # 结束
    connexion.close()
    return "Message envoye"

# --------------------------------------------------------------------------
def sendCommand(connexion,commande,verbose,withRCLF):
    # 在连接频道中发送命令
    # 如果 verbose=1,则启用详细模式
    # 如果 withRCLF=1,则将序列 RCLF 添加到命令中

    # 数据
    RCLF="\r\n" if withRCLF else ""
    # 若命令不为空,则发送命令
    if commande:
        connexion.send("%s%s" % (commande,RCLF))
        # 可能的回显
        if verbose:
            affiche(commande,1)
        # 读取少于 1000 个字符的响应
        reponse=connexion.recv(1000)
        # 可能的回显
        if verbose:
            affiche(reponse,2)
        # 获取错误代码
        codeErreur=reponse[0:3]
        # 服务器返回错误?
        if int(codeErreur) >=500:
            return reponse[4:]
        # 无错误返回
        return ""

# --------------------------------------------------------------------------
def affiche(echange,sens):
    # 在屏幕上显示交互内容?
    # 若 sens=1 则显示 --> 交换
    # 如果 sens=2,则显示 <-- 交换,不包括最后两个字符 RCLF
    if sens==1:
        print "--> [%s]" % (echange)
        return
    elif sens==2:
        l=len(echange)
        print "<-- [%s]" % echange[0:l-2]
        return

      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # 如果存在,则删除 [ligne] 中的行尾标记
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# 主 ----------------------------------------------------------------
  
# 客户端 SMTP(SendMail 传输协议),用于发送消息
# 信息取自一个名为 INFOS 的文件,其中包含以下行
# 第 1 行:smtp、发件人、收件人
# 后续行:消息正文

# 发件人:发件人邮箱
# 收件人:收件人邮箱
# smtp:要使用的 smtp 服务器名称


# 通信协议 SMTP 客户端-服务器
# -> 客户端连接到 SMTP 服务器的 25 号端口
# <- 服务器向其发送欢迎信息
# -> 客户端发送命令 EHLO:其主机名
# <- 服务器返回 OK 或不返回
# -> 客户端发送邮件指令:mail from: <发件人>
# <- 服务器响应 OK 或不响应
# -> 客户端发送 rcpt to: <收件人> 命令
# <- 服务器响应 OK 或不响应
# -> 客户端发送 data 命令
# <- 服务器响应 OK 或不响应
# -> 客户端发送消息中的所有行,并以仅包含单个字符的行结尾。
# <- 服务器响应 OK 或不响应
# -> 客户端发送 quit 命令
# <- 服务器响应 OK 或不响应

# 服务器的响应格式为 xxx 文本,其中 xxx 是三位数。所有 xxx 数值 >=500
# 表示发生错误。响应可能包含多行,除最后一行外均以 xxx- 开头
# 格式为 xxx(空格)

# 交换的文本行必须以字符 RC(#13) 和 LF(#10) 结尾

# # 邮件发送参数
MAIL="mail2.txt" 
# 获取邮件参数
res=getInfos(MAIL)
# 错误?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# 以详细模式发送邮件
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# 结束
sys.exit()

  • 在安装了杀毒软件的 Windows 机器上,杀毒软件可能会阻止 Python 脚本连接到 SMTP 服务器的 25 号端口。此时需要禁用杀毒软件。以 McAfee 为例,操作步骤如下:
  • 在 [1] 上,启用 VirusScan 控制台
  • 在 [2] 时,停止 [Protection lors de l'accès] 服务
  • 在 [3] 时,服务已停止

结果

文件 infos.txt

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

屏幕显示结果:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-angers.fr]
--> [EHLO Gportpers3.ad.univ-angers.fr]
<-- [220 smtp.univ-angers.fr ESMTP Postfix
250-smtp.univ-angers.fr
250-PIPELINING
250-SIZE 20480000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN]
--> [MAIL FROM: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.0 Ok]
--> [RCPT TO: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.5 Ok]
--> [DATA]
<-- [354 End data with <CR><LF>.<CR><LF>]
--> [From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

.
]
<-- [250 2.0.0 Ok: queued as 63412114203]
--> [QUIT]
<-- [221 2.0.0 Bye]
Resultat de l'envoi : Message envoye

Thunderbird邮件客户端读取的消息:

  

11.4. 第二个 SMTP 客户端

此第二个脚本与前一个功能相同,但使用了模块 [smtplib] 的功能。


程序 (inet_04)


# -*- coding=utf-8 -*-

import sys,socket, smtplib

#-----------------------------------------------------------------------
def getInfos(fichier):
    # 返回从文本文件中提取的信息(SMTP、发件人、收件人、邮件内容)[fichier]
    # 第 1 行:SMTP、发件人、收件人
    # 后续行:邮件正文

    # 打开 [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # 读取第一行
    ligne=infos.readline()
    # 删除换行符
    ligne=cutNewLineChar(ligne)
    # 提取 smtp、发件人、收件人字段
    champs=ligne.split(",")
    # 字段数量是否正确?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # 对提取的信息进行“处理”——去除其前后的“空格”
    for i in range(3):
        champs[i]=champs[i].strip()
    # 字段提取
    (smtpServer,expediteur,destinataire)=champs
    # 读取待发送的消息
    message=""
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # 返回
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # 代表发件人将消息发送至 SMTP 服务器 smtpserver
    # 发送给收件人。如果 verbose=True,则记录客户端与服务器的交互

    # 使用 smtplib 库 
    try:
        server = smtplib.SMTP(smtpServer)
        if verbose:
            server.set_debuglevel(1)
            server.sendmail(expediteur, destinataire, message)
            server.quit()
    except Exception, erreur:
        return "Erreur envoi du message : %s" % (erreur)
    # 结束
    return "Message envoye"
      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # 若存在,则删除 [ligne] 中的行尾标记
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# 主程序 ----------------------------------------------------------------
  
# SMTP 客户端(SendMail 传输协议),用于发送消息
# 信息取自一个名为 INFOS 的文件,其中包含以下行
# 第 1 行:smtp、发件人、收件人
# 后续行:消息正文

# 发件人:发件人邮箱
# 收件人:收件人邮箱
# smtp:要使用的 smtp 服务器名称


# 通信协议 SMTP 客户端-服务器
# -> 客户端连接到 SMTP 服务器的 25 号端口
# <- 服务器向其发送欢迎信息
# -> 客户端发送命令 EHLO:其主机名
# <- 服务器返回 OK 或不返回
# -> 客户端发送邮件指令:mail from: <发件人>
# <- 服务器响应 OK 或不响应
# -> 客户端发送 rcpt 命令,收件人:<收件人>
# <- 服务器响应 OK 或不响应
# -> 客户端发送 data 命令
# <- 服务器响应 OK 或不响应
# -> 客户端发送消息中的所有行,并以仅包含单个字符的行结尾。
# <- 服务器响应 OK 或不响应
# -> 客户端发送 quit 命令
# <- 服务器响应 OK 或不响应

# 服务器的响应格式为 xxx 文本,其中 xxx 是一个三位数。所有 xxx 数值均 >=500
# 表示发生错误。响应可能包含多行,除最后一行外均以 xxx- 开头
# 格式为 xxx(空格)

# 交换的文本行必须以字符 RC(#13) 和 LF(#10) 结尾

# # 邮件发送参数
MAIL="mail2.txt" 
# 获取邮件参数
res=getInfos(MAIL)
# 错误?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# 以详细模式发送邮件
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# 结束
sys.exit()

注:

  • 除sendmail函数外,该脚本与前一个完全相同。该函数现使用[smtplib]模块的功能(第3行)。

结果

文件 mail2.txt

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

屏幕显示结果:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-
angers.fr]
send: 'ehlo Gportpers3.ad.univ-angers.fr\r\n'
reply: '250-smtp.univ-angers.fr\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-SIZE 20480000\r\n'
reply: '250-VRFY\r\n'
reply: '250-ETRN\r\n'
reply: '250-ENHANCEDSTATUSCODES\r\n'
reply: '250-8BITMIME\r\n'
reply: '250 DSN\r\n'
reply: retcode (250); Msg: smtp.univ-angers.fr
PIPELINING
SIZE 20480000
VRFY
ETRN
ENHANCEDSTATUSCODES
8BITMIME
DSN
send: 'mail FROM:<serge.tahe@univ-angers.fr> size=99\r\n'
reply: '250 2.1.0 Ok\r\n'
reply: retcode (250); Msg: 2.1.0 Ok
send: 'rcpt TO:<serge.tahe@univ-angers.fr>\r\n'
reply: '250 2.1.5 Ok\r\n'
reply: retcode (250); Msg: 2.1.5 Ok
send: 'data\r\n'
reply: '354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: End data with <CR><LF>.<CR><LF>
data: (354, 'End data with <CR><LF>.<CR><LF>')
send: 'From: serge.tahe@univ-angers.fr\r\nTo: serge.tahe@univ-angers.fr\r\nSubje
ct: test\r\n\r\nligne1\r\nligne2\r\n\r\nligne3\r\n.\r\n'
reply: '250 2.0.0 Ok: queued as D4977114358\r\n'
reply: retcode (250); Msg: 2.0.0 Ok: queued as D4977114358
data: (250, '2.0.0 Ok: queued as D4977114358')
send: 'quit\r\n'
reply: '221 2.0.0 Bye\r\n'
reply: retcode (221); Msg: 2.0.0 Bye
Resultat de l'envoi : Message envoye

11.5. 回显客户端/服务器

创建一个回显服务。服务器将客户端发送的所有文本行以大写形式返回。该服务可通过使用线程同时为多个客户端提供服务。


服务器程序(inet_08)


# -*- coding=utf-8 -*-

# Windows 上的通用 TCP 服务器

# 加载头文件
import re,sys,SocketServer,threading

# 多线程 TCP 服务器
class ThreadedTCPRequestHandler(SocketServer.StreamRequestHandler):

  def handle(self):
    # 当前线程
    cur_thread = threading.currentThread()
    # 客户端数据
    self.data="on"
    # 在空字符串处停止
    while self.data:
      # 使用 readline 方法读取客户端文本行
      self.data =self.rfile.readline().strip()
      # 控制台跟踪
      print "client %s : %s (%s)" % (self.client_address[0],self.data,cur_thread.getName())
      # 向客户端发送响应
      response = "%s: %s" % (cur_thread.getName(), self.data.upper())
      # self.wfile 是向客户端的写入流
      self.wfile.write(response)                    

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

# ------------------------------------------------------ main        
# 调用语法:argv[0] 端口
# 服务器在指定端口上启动

# 数据
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- 验证调用
# 必须且仅有一个参数
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# 端口必须是数字
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# 启动服务器 - 每个客户端由一个线程处理
host="localhost"
server = ThreadedTCPServer((host,int(port)), ThreadedTCPRequestHandler)

# 服务器在单个线程中启动
# 每个客户端将在一个额外的线程中得到服务
server_thread = threading.Thread(target=server.serve_forever)
# 启动服务器 - 无限循环等待客户端
server_thread.start()
# 后续
print "Serveur d'echo a l'ecoute sur le port % s" % (port)

注:

  • 第 39 行:sys.argv 代表脚本参数。此处的参数格式应为:nom_du_script 端口号。因此必须有两个参数。 sys.argv[0] 将变为 nom_du_script,而 sys.argv[1] 将变为 port ;
  • 第 53 行:回显服务器是 ThreadedTcpServer 类的实例。该类的构造函数需要两个参数:
    • 参数 1:一个包含两个元素 (host,port) 的元组,用于确定服务器的机器和监听端口;
    • 参数 2:负责处理客户端请求的类名。
  • 第 57 行:创建了一个线程(但尚未启动)。该线程将执行 TCP 服务器中的 [serve_forever] 方法。该方法是一个监听客户端连接的循环。 一旦检测到客户端连接,将创建 ThreadedTCPRequestHandler 类的实例。其 handle 方法负责与客户端进行交互;
  • 第 59 行:启动回显服务线程。从此时起,客户端即可连接到该服务;
  • 第 27 行:回显服务器的类。它继承自两个类:SocketServer.ThreadingMixInSocketServer.TCPServer。 这使其成为一个多线程的 TCP 服务器:服务器在主线程中运行,每个客户端请求则在单独的子线程中处理;
  • 第9行:处理客户端请求的类。它继承自SocketServer.StreamRequestHandler类,因此继承了两个属性:
    • rfile:这是客户端发送数据的读取流——可视为文本文件;
    • wfile:用于向客户端发送数据的写入流——可作为文本文件处理。
  • 第 11 行:方法 handle 处理客户端的请求;
  • 第 13 行:执行此方法的线程 handle;
  • 第 17 行:处理客户端请求的循环。当客户端发送空行时,循环结束;
  • 第 19 行:读取客户端请求;
  • 第 21 行:self.client_address[0] 表示客户端的地址 IP。cur_thread.getName() 是执行 handle 方法的线程名称
  • 第 23 行:对客户端的响应包含两个部分——服务该客户端的线程名称,以及客户端发送的命令(以大写形式传递)。

客户端程序(inet_06)


# -*- coding=utf-8 -*-
import re,sys,socket

# ------------------------------------------------------ main        
# 通用 TCP 客户端
# 调用语法:argv[0] 主机 端口
# 客户端连接到回显服务(主机,端口)
# 服务器返回客户端输入的行

# 语法
syntaxe="syntaxe : %s hote port" % (sys.argv[0])

#------------------------------------- 验证调用
# 必须有两个参数
nbArguments=len(sys.argv)
if nbArguments!=3:
    print syntaxe
    sys.exit(1)

# 获取参数
hote=sys.argv[1]
# 端口必须是数字
port=sys.argv[2]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s foit être un nombre entier positif" % (port)
    sys.exit(2)

try:
  # 客户端与服务器的连接
  connexion=socket.create_connection((hote,int(port)))
except socket.error, erreur:
  print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
  sys.exit(3)
  
try:
    # 输入循环
    ligne=raw_input("Commande (rien pour arreter): ").strip()
    while ligne!="":
        # 将行发送至服务器
        connexion.send("%s\n" % (ligne))
        # 等待响应
        reponse=connexion.recv(1000)
        print "<-- %s" % (reponse)
        ligne=raw_input("Commande (rien pour arreter): ")
except socket.error, erreur:
    print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
    sys.exit(3)
finally:
    # 关闭连接
    connexion.close()

结果

服务器在第一个命令窗口中启动:

cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100

第一个客户端在第二个窗口中启动:

1
2
3
4
5
6
7
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 1
[cmde 1 du client 1]
<-- Thread-2: CMDE 1 DU CLIENT 1
Commande (rien pour arreter): cmde 2 du client 1
<-- Thread-2: CMDE 2 DU CLIENT 1
Commande (rien pour arreter):

客户端成功收到了服务器对其发送的命令的响应,该响应正是该命令的全大写形式。第二个客户端在第三个窗口中启动:

1
2
3
4
5
6
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 2
<-- Thread-3: CMDE 1 DU CLIENT 2
Commande (rien pour arreter): cmde 2 du client 2
<-- Thread-3: CMDE 2 DU CLIENT 2
Commande (rien pour arreter):

此时服务器的控制台显示如下:

1
2
3
4
5
6
cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100
client 127.0.0.1 : cmde 1 du client 1 (Thread-2)
client 127.0.0.1 : cmde 2 du client 1 (Thread-2)
client 127.0.0.1 : cmde 1 du client 2 (Thread-3)
client 127.0.0.1 : cmde 2 du client 2 (Thread-3)

客户端确实在不同的线程中运行。要停止一个客户端,只需输入一个空命令即可。

11.6. 通用 TCP 服务器

我们计划编写一个 Python 脚本,该脚本

  • 它将是一个TCP服务器,能够同时服务一个客户端,
  • 能够接收客户端发送的文本行,
  • 并接受来自键盘的文本行,将其作为响应发送给客户端。

因此,键盘前的用户充当了服务器:

  • 他在控制台上看到客户端发送的文本行;
  • 并通过键盘输入回复来回应客户端。

因此,它能够适应任何类型的客户端。正因如此,我们将它称为“通用TCP服务器”。这是一个用于探索TCP通信协议的实用工具。在下面的示例中,TCP客户端将是一个网页浏览器,这将使我们能够探索Web客户端使用的HTTP协议。


程序(inet_10)


# -*- coding=utf-8 -*-

# 通用 TCP 服务器

# 加载头文件
import re,sys,SocketServer,threading

# 通用 TCP 服务器
class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # 显示客户端
        print "client %s" % (self.client_address[0])
        # 创建读取客户端命令的线程
        thread_lecture = threading.Thread(target=self.lecture)
        thread_lecture.start()
        # 收到“bye”命令后停止
        # 读取键盘输入的命令
        cmde=raw_input("--> ")
        while cmde!="bye":
            # 向客户端发送命令。self.wfile 是向客户端的写入流
            self.wfile.write("%s\n" % (cmde))
            # 读取下一条命令
            cmde=raw_input("--> ")

    def lecture(self):
        # 显示客户端发送的所有行,直到收到 bye 命令
        ligne=""
        while ligne!="bye":
            # 使用 readline 方法读取客户端的文本行
            ligne = self.rfile.readline().strip()
            # 控制台跟踪
            print "<--- %s : %s" % (self.client_address[0], ligne)
    
# ------------------------------------------------------ main        
# 调用语法:argv[0] 端口
# 服务器在指定端口上启动

# 数据
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- 验证调用
# 必须且仅有一个参数
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# 端口必须是数字
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# 启动服务器
host="localhost"
server = SocketServer.TCPServer((host, int(port)), MyTCPHandler)
print "Service tcp generique lance sur le port %s. Arret par Ctrl-C" % (port)
server.serve_forever()

注:

  • 第 57 行:TCP 服务器将是一个 SocketServer.TCPServer 类的实例。该类的构造函数接受两个参数:
    • 第一个参数是一个包含两个元素的元组(主机名,端口号),其中 host 是运行该服务的机器(通常为 localhost),而 port 是服务等待(监听)客户端请求的端口;
    • 第二个参数指定客户端的服务类。当客户端连接时,会创建该服务类的实例,并由其方法 handle 负责管理与客户端的连接。

TCP 服务器 SocketServer.TCPServer 并非多线程的。它每次只服务一个客户端;

  • 第 59 行:执行 TCP 服务器的 serve_forever 方法。这是一个无限循环,用于等待客户端;
  • 第 9 行:此处的 TCP 服务器是 SocketServer.StreamRequestHandler 类的派生类。这使得与客户端交换的数据流可以被视为文本文件。我们之前已经遇到过这个类。我们拥有以下方法:
    • readline 用于读取来自客户端的一行文本;
    • write 用于向客户端发送文本行。
  • 第 12 行:self.client_address[0] 是客户端的 IP 地址;
  • 第 14 行:TCP 服务器将通过两个线程与客户端进行通信
    • 一个用于读取客户端行数据的线程;
    • 一个向客户端写入行数据的线程。
  • 第 14 行:创建用于读取客户端行数据的线程。其参数 target 确定了该线程执行的方法,即第 25 行定义的方法;
  • 第 25 行:启动读取线程;
  • 第25-32行:读取线程执行的方法;
  • 第28行:读取线程读取客户端发送的所有文本行,直到接收到“bye”这一行;
  • 第 18 行:此处处于向客户端写入的线程中。其原理是将用户通过键盘输入的所有文本行发送给客户端。

运行结果

服务器

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C

客户端浏览器

 

服务器接收到的请求

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C
client 127.0.0.1
<--- 127.0.0.1 : GET / HTTP/1.1
<--> --- 127.0.0.1 : Host: localhost:100
<--- 127.0.0.1 : User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 GTB7.1 ( .NET CLR 3.5.30729)
<--- 127.0.0.1 : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
<--- 127.0.0.1 : Accept-Language: fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3
<--- 127.0.0.1 : Accept-Encoding: gzip,deflate
<--- 127.0.0.1 : Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
<--- 127.0.0.1 : Keep-Alive: 115
<--- 127.0.0.1 : Connection: keep-alive
<--- 127.0.0.1 : Cache-Control: max-age=0
<--- 127.0.0.1 :

用户通过键盘输入的服务器响应(不含 --> 符号)

1
2
3
4
5
6
7
HTTP/1.1 200 OK
--> Server: serveur tcp generique
--> Connection: close
--> Content-Type: text/html
-->
--> <html><body><h2>Serveur tcp generique</h2></body></html>
--> bye
  • 第1-5行:发送给客户端的HTTP响应;
  • 第 6 行:发送给客户端的 HTML 页面;
  • 第7行:与客户端的对话结束。客户服务将终止,连接将被关闭,这将突然中断读取客户端发送的文本行的线程;
  • 发给客户端的HTTP响应的第1-5行含义如下:
  • 第1行:已找到客户端请求的资源;
  • 第2行:服务器标识;
  • 第3行:服务器将在发送资源后关闭连接;
  • 第 4 行:服务器发送的资源类型:HTML 文档;
  • 第5行:空行。

[1] 浏览器显示的页面:

如果查看 [2] 浏览器接收到的源代码,会发现该网页浏览器接收到的代码 HTML 确实就是我们发送给它的内容。