python网络编程——TCP

本文深入解析TCP传输控制协议,包括其工作原理、套接字机制、客户端与服务器通信流程,以及常见问题如地址冲突、死锁和半开连接的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.TCP。

TCP叫做传输控制协议,是OSI七层参考模型中传输层使用的协议,传输文档和文件的协议几乎都是使用TCP。

2.TCP工作原理。

TCP不像UDP一样提供尽最大可能的交付,它提供的是可靠传输。以下是TCP提供可靠传输的基本原理:

  • 每个TCP数据包都有一个序列号,接收方通过序列号将响应数据包正确排序。也可通过该序列号发现传输序列中丢弃的数据包,并请求重传。
  • TCP并不使用顺序的整数作为序列号,而是通过一个计数器来记录发送的字节数。
  • 一个优秀的TCP实现中,初始序列号是随机选择的。这样就给伪造数据包加大了难度。
  • TCP无须等待响应就能一口气发送多个数据包。在某一时刻发送方希望同时传输的数据量叫做TCP窗口的大小。
  • 接收方的TCP实现可以通过控制发送方的窗后大小来减缓或短暂暂停连接。这叫作流量控制。这使得接收方在输入缓冲区已满时可以禁止更多数据包的传输。此时如果还有数据到达的话,那么这些数据也会被丢弃。
  • 如果TCP认为数据包被丢弃了,它会假定网络正在变得拥挤,然后减少每秒发送的数据量。

在两台主机之间建立TCP连接需要三个数据包,这三个数据包组成了一个序列——SYN,SYN-ACK,ACK。

  • SYN:“我想进行通信,这是数据包的初始序列号。”
  • SYN-ACK:“好的,这是我向你发送数据包的初始序列号”
  • ACK:“好的”

这就是俗称的TCP的三次握手。

3.TCP套接字。

  • 使用TCP这样支持状态的流协议,connect()调用是后续所有网络通信所依赖的首要步骤。只有操作系统的网络栈成功完成TCP三次握手,TCP流的双方才算做好了通信的准备。
  • TCP的标准POSIX接口实际上包含两种截然不同的套接字类型:“被动”监听套接字和主动“连接”套接字。
    • 被动套接字又叫做监听套接字。服务器通过该套接字来接受连接请求,但是该套接字不能用于发送和接受任何数据,也不表示任何实际的网络会话。而是由服务器指示被动套接字通知操作系统首先使用哪个特定的TCP端口号来接受连接请求。

    • 主动套接字又叫作连接套接字。他将一个特定的IP地址及端口号和某个与其进行远程会话的主机绑定。该套接字只用于与该特定远程主机进行通信,可以通过该套接字发送或接受数据。

被动套接字由接口IP地址和正在监听的端口号来唯一标识,因此任何其他应用程序都无法在使用相同的IP地址和端口。但是,多个主动套接字是可以共享一个本地套接字名的。

4.一个简单的TCP客户端和服务器。

代码:

#!/usr/bin/python
#coding:utf-8
import argparse,socket
#编写接收数据函数得功能使其能设置接受特定长度数据的的功能
def recvall(sock,length):
    data = b''
    while len(data) < length:
        more = sock.recv(length - len(data))
        if not more:
            raise EOFError('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:
        print('Waiting to accept a new connection')
        #accept()方法唯一目的就是用于支持TCP套接字的监听功能
        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 sixteen-octet message:',repr(message))
        #使用sendall()python线程在所有数据发送完之前不会竞争资源
        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'Hi there, server')
    reply = recvall(sock,16)
    #repr()方法可以观察转义字符,不会翻译转义字符
    print('The server said',repr(reply))
    sock.close()

if __name__ == '__main__':
    choices = {'client':client,'server':server}
    parser = argparse.ArgumentParser(description='Send and receive over TCP')
    parser.add_argument('role',choices=choices,help='Which role to play')
    parser.add_argument('host',help='interface the server listens at;'
                        'host the client sends to')
    parser.add_argument('-p',metavar='PORT',type=int,default=1060,help='TCP port (default 1060)')

    args = parser.parse_args()
    function = choices[args.role]
    function(args.host,args.p)

首先服务器调用bind()来声明一个特定的端口,它还没决定该程序到底会作为服务器还是客户端。接着调用listen()该程序通过调用它,表明希望套接字进行监听,此时才真正决定了程序要作为服务器。listen()调用对套接字是无法撤销的,而且调用之后该套接字再也不能用于发送和接收数据,该套接字此时只能通过accept()方法来接受连接请求。
测试结果:
在这里插入图片描述

5.地址已被占用。

服务器在绑定端口之前进行了这句代码:

       sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

通过设定套接字选项SO_REUSEADDR,可以指明,应用程序能够使用一些网络客户端之前的连接正在关闭的端口,否则上一个套接字将无法立即消失导致连接失败。

6.死锁。

当两个程序共享有限资源时,由于糟糕的计划,只能一直等待对方结束资源占用,这种情况就是死锁,TCP栈使用了缓冲区,这样就可以在应用程序准备好读取数据前存放接收到的数据,也可以在网络硬件准备好发送数据包前存放要发送的数据。但是这些缓冲区大小是有限制的,如果有太多服务器和客户端未来得及处理的数据就会造成麻烦。下面是一个可能造成死锁的TCP服务器和客户端。
代码:

#!/usr/bin/python
#coding:utf-8
import argparse,sys,socket

def server(host,port,bytecount):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET,socket.SOCK_STREAM,1)
    sock.bind((host,port))
    sock.listen(1)
    print('Listening at',sock.getsockname())
    while True:
        sc,sockname = sock.accept()
        print('Processing up to 1024 bytes at a time from',sockname)
        n = 0
        while True:
            data = sc.recv(1024)
            if not data:
                break
            output = data.decode('ascii').upper().encode('ascii')
            sc.sendall(output)
            n += len(data)
            print('\r %d bytes processed so far' % (n,), end=' ')
            #刷新输出这句代码只在Linux系统下起作用
            sys.stdout.flush()
        print()
        sc.close()
        print(' Socket closed')

def client(host,port,bytecount):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # // 表示商取整如7//2=3
    bytocount = (bytecount + 15) // 16 * 16
    message = b'capitalize this!'

    print('Sending', bytecount, 'bytes of data,in chunks of 16 bytes')
    sock.connect((host,port))
    sent = 0
    while sent < bytecount:
        sock.sendall(message)
        sent += len(message)
        print('\r  %d bytes sent' % (sent,), end=' ')
        sys.stdout.flush()

    print()
    #表示客户端不再向套接字写入数据,而服务端也不会再读取任何数据并认为遇到了文件结束符
    sock.shutdown(socket.SHUT_WR)

    print('Receiving all the data the server sends back')

    received = 0
    while True:
        data = sock.recv(42)
        if not received:
            #repr()方法可以观察转义字符,不会翻译转义字符
            print(' The first data received says',repr(data))
        if not data:
            break
        received += len(data)
        print('\r %d bytes recevied' % (received,), end=' ')

    print()
    sock.close()

if __name__ == '__main__':
    choices = {'client':client,'server':server}
    parser = argparse.ArgumentParser(description='Get deadlocked over TCP')
    parser.add_argument('role',choices=choices,help='Which role to play')
    parser.add_argument('host',help='interface the server listens at;'
                        'host the client sends to')
    parser.add_argument('bytecount', type=int,nargs='?',default=16,help='number of bytes for client to send (default 16)')
    parser.add_argument('-p',metavar='PORT',type=int,default=1060,help='TCP port (default 1060)')

    args = parser.parse_args()
    function = choices[args.role]
    function(args.host,args.p,args.bytecount)

该服务器的任务是将任意数量的文本转换为大写形式。由于客户端的请求量可能非常的大,如果试图读取整个输入流再做处理的话,可能会耗尽系统内存。因此,该服务器每次只读取并处理1024字节的小型数据块。
测试结果:
在这里插入图片描述
如果设置发送很大的数据将造成客户端和服务器的死锁。
在这里插入图片描述

7.已关闭连接,半开连接。

”半关”,即在一个方向上永久关闭通信连接,但不销毁套接字。这种状态下服务器再也不会读取任何数据,但它能继续向客户端发送剩余数据。shutdown()调用解决了服务器在遇到文件结束符之前永远读取数据,而客户端不会在套接字上进行完整的close(),也防止了运行很多recv()调用来接受服务器响应。下面是shutdown()的常用参数:

  • SHUT_WR:表示调用方将不再向套接字写入数据,而通信对方也不会再读取任何数据并认为遇到了文件结束符。
  • SHUT_RD:用来关闭接受方向的套接字流。
  • SHUT_RDWR:将套接字两个方向的通信都关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晶晶娃在战斗

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值