Python网络编程 3.1 由简单的TCP服务器、客户端程序分析相关方法

对比着github中的源码,自己敲出来的代码如下:

import argparse,socket

def recvall(sock,length):
    data = b''
    while len(data) < length:
        more = sock.recv(length-len(data))
        if not more:
            raise IOError('was expecting %d bytes but only received %d bytes before the socket closed'%(length,len(data)))
        data += more
    return data
def server(interface,port):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sock.bind((interface,port))
    sock.listen(1)
    print('listening at ',sock.getsockname())
    while True:
        sc,sockname = sock.accept()
        print('we have accepted a connection from',sockname)
        print('Socket name:',sc.getsockname())
        print('Socket peer:',sc.getpeername())
        message = recvall(sc,16)
        print('Incoming 16-octet mesaage:',repr(message))
        sc.sendall(b'Farewell,client')
        sc.close()
        print('Reply sent,socket closed')

def client(host,port):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.connect((host,port))
    print('client has been assigned socket name',sock.getsockname())
    sock.sendall(b'Hello Server')
    reply = recvall(sock,16)
    print('The server said',repr(reply))
    sock.close()

if __name__ == '__main__':
    choices = {'server':server,'client':client}
    parser = argparse.ArgumentParser(description='Send and receive over TCP')
    parser.add_argument('role',choices=choices)
    parser.add_argument('host')
    parser.add_argument('-p',metavar='PORT',type=int,default=1060)
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host,args.p)


 
在命令行中运行的命令及效果:


可以看出,这两个程序的运行结果并不是预想中的结果,服务端只打印了socket name和socket peer,客户端只打印了一句话,而后面的那些内容都没有打印出来。本以为是程序与操作系统的兼容性问题,但是把网站中的源码(chapter03中的tcp_sixteen.py)拉下来运行结果却是符合预期的,如图


这就很令人费解了,由于肉眼看不太好看出两端代码的区别,就分别放到了word文档,按照word文档对比方法中的步骤对两端代码进行比对。


经过仔细的对比观察,发现错误有三处,一处是将EOFError写成了IOError,改了之后仍然运行不成功。剩下两处分别是server和client中sock.sendall()方法里的内容区别,我的代码的内容的字节数并不是16字节。当把内容的字节数改为16字节的时候,发现也能成功运行了。

那又出现了一个问题,为什么字节数没有对上,却没有出现recvall中的EOFError?为了研究这个问题,我们分别对server和client两个方法中的recvall中的参数进行改动,改为(10,10)(20,20)(10,20),会发现分别有不同的结果,但是都没有出现EOFError。猜测也许是在TCP服务出现问题的时候才会出现该error.

通过上述程序可以看出:

1) argparse的用法:可以参照argparse实例详解文章来进行学习。其中在本文涉及到的源码中,要注意主程序部分choices和末尾function的用法,以及argparse中可选项的表示和赋值方法。  我对源代码加了一些自己的注释,放在了GitHub-P&K中,是 tcp_server_client.py 

2)    客户端连接并不需要输入端口,只需要输入想要连接的某IP和对应端口,客户端主机的操作系统会为其分配一个端口用以和客户端程序绑定,而服务器端的端口即是服务器端的主机与客户端通信所用到的端口,在程序中服务器的端口赋了一个默认值1060,也可以通过运行的时候写入一个值来指定其监听的端口号。

3) 每个会话使用一个套接字:

    a) 有两种流套接字:监听套接字和连接套接字。服务器通过监听套接字设定某个端口用于监听连接请求;连接套接字用于表示服务器与某一特定客户端正在进行的会话。

    b) 监听套接字调用accept()后,实际上返回了一个新建的连接套接字。以程序为例,看看套接字操作发生的顺序:

     首先,服务器通过运行bind(),简单地声明一个特定的端口。要注意,此时还没有决定该程序会被用作服务器还是客户端,即没有确定该程序是被动地等待接收连接请求,还      是主动发出连接请求。

     下一个调用是listen()。程序通过调用该方法,表明它希望通过套接字进行监听,此时才真正决定了程序要作为服务器。在TCP套接字上运行该调用会彻底转变套接字的角            色,且listen()调用对套接字的改变是无法撤销的,而且调用之后该套接字再也不能用于发送或接收数据,这一特定的套接字对象将再也不会与任何特定的客户端连接。该套        接字只能通过它的accept()方法来接受连接请求(accept方法的唯一目的就是用于支持TCP套接字的监听功能)。每次调用accept()都会等待一个新的客户端连接服务器,然        后返回一个全新的套接字,该新建的套接字负责管理对应的新建会话。

     可以从代码中看出,accept()方法返回的连接套接字与客户端套接字的通信模式是完全相同的。注意到listen()方法中的参数为1。该参数表明了处于等待的连接的最大数目。        当栈中等待的连接超过了该参数设置的最大等待数,操作系统就会忽略新的连接请求,并推迟后续的三次握手。

4) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  这一句代码中, REFUSEADDR表示允许多个实例绑定同一个端口。详细来说,其可以指明应用程序能够使用一些网络客户端之前的连接正在关闭的端口。

5) 绑定端口:sock.bind((interface,port))中,要注意该方法的作用是将服务器与该端口绑定,并且interface指明了服务器要监听的网络接口。即,如果是interface 的值是localhost的话,那么表示服务器不会接收到来自其他机器的连接请求,不会有任何响应。如果interface的值为一个空字符串,即两个双引号""则表示服务器可以接收来自任何机器的连接请求。

6)    TCP与UDP的一个不同之处,在于UDP应用程序接收到的只可能是一个完整的数据包,因为UDP的数据报都是原子的,数据报中的数据是自包含的,只有成功发送/接收或者失败两种情况,不可能只发送一半或只接收一半。而TCP可能会在传输中把数据流分为多个大小不同的数据包,然后在接收端进行逐步重组,因此,在进行TCP的send和recv方法调用时可能会有不同的情况,先看send():

    a)   要发送的数据可能立刻被本地系统的网络栈接收。这可能是因为网卡刚好处于空闲,或者系统还有空间可以将数据复制到发送缓冲区。这样程序就能继续运行了,在这些     情况下send()方法返回值是整个字符串的长度。

    b)  如果网卡正忙,该套接字的发送缓冲区也已经满了,且系统不能或不愿为其分配更多的空间。此时send()方法默认情况下会阻塞该进程,暂停应用程序,直到本地网络栈       能够接收并传输数据。

    c)    介于上述两种情况之间,发送缓冲区几乎满了,但尚有空闲,因此想要发送的一部分数据可以进入发送缓冲区队列等待发送。但剩余的数据块则必须等待。这种情况下,     send()会立即返回从数据串中开始处起已经被接收的字节数,剩余数据则尚未被处理。

由于可能发生最后一种情况,在调用流套接字的send()时应该检查返回值,还需要在一个循环内进行send()调用。这样,如果发生部分传输的现象,程序就会不断尝试发

送剩余的数据,直至整个字节串被发送。有时会在网络程序的代码中看到如下形式的循环:

bytes_sent = 0 
while bytes_sent < len(message):
   message_remaining = message[bytes_sent:]
   bytes_sent += s.send(message_remaining)
 
 

除此之外,Python标准库的socket实现提供了一个友好的sendall()方法,因为sendall()方法是用C实现的,所以它比我们自己实现的循环要快。另外,它在循环中释放了全局解释器锁,因此其他的Python线程在所有数据发送完成前不会竞争资源。

recv()方法:

a)    如果没有任何数据,那么recv()会阻塞程序,直到有数据传到。

b)   如果接收缓冲区内的数据已经完整就绪,那么recv()会接收所需的所有数据。

c)    如果接收缓冲区中只有recv()需要返回的部分数据,那么,即使这并非所需的全部内容,recv()也会立即返回缓冲区已有的数据。

因此,必须在一个循环中调用recv()方法。它无法猜测传来的数据何时能够组成程序所需的完整信息,因此它收到数据后会立刻返回。因此,并没有一个标准的类似于

sendall()方法的recvall()方法。

  

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值