天天看点

网络编程(part9)--socket套接字编程之TCP套接字

鄙人学习笔记

文章目录

  • ​​套接字介绍​​
  • ​​定义​​
  • ​​套接字分类(针对TCP和UDP的分类)​​
  • ​​TCP套接字编程​​
  • ​​服务端流程​​
  • ​​代码实现​​
  • ​​举个例子​​
  • ​​客户端流程​​
  • ​​代码实现​​
  • ​​举个例子​​
  • ​​TCP套接字数据传输特点​​
  • ​​做个练习​​
  • ​​网络收发缓冲区​​
  • ​​举个例子​​
  • ​​TCP粘包​​

套接字介绍

定义

套接字是实现网络编程进行数据传输的一种技术手段

套接字分类(针对TCP和UDP的分类)

①流式套接字(SOCK_STREAM): 以字节流方式(就像是管道中的水流一样)传输数据,实现TCP网络传输方案。

②数据报套接字(SOCK_DGRAM):以数据报形式(就像是用瓶子打包好的水一样)传输数据,实现UDP网络传输方案。

TCP套接字编程

服务端流程

先来看一个流程图:

网络编程(part9)--socket套接字编程之TCP套接字

代码实现

  1. 创建套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)      
功能:创建套接字对象
参数:  
socket_family  网络地址类型:AF_INET表示ipv4
socket_type  套接字类型:SOCK_STREAM 流式;SOCK_DGRAM 数据报
proto(在应用层网络编程中用不到) 选择子协议:通常为0  
返回值: 套接字对象      

备注:底层/系统层套接字也针对某种协议,这些协议,有的时候会产生一些分支,也就是子协议。那么为啥在应用层网络编程中用不到proto这个参数呢?因为TCP协议和UDP协议没有子协议,所以参数设为0,即不选择任何子协议。

  1. 绑定地址
sockfd.bind(addr)      
功能: 绑定本机网络地址(如果不写本机网络地址,则会报错)
参数: 二元元组 (ip,port)  比如('0.0.0.0',8888)      

备注:addr为一个元组,这个元组有两个元素一个是网络地址(IP),一个是端口号(port)。

  1. 设置监听
sockfd.listen(n)      
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小      

备注1:并不是所有的套接字都具备监听的功能,即被客户端连接的功能。通过调用listen,我们的套接字对象才能被客户端连接。

备注2:一个服务端套接字对象可以同时连接多个客户端,与客户端进行连接需要进行3次握手,且连接的过程需要一个一个来进行。比如说,同时有5个客户端发起了连接请求,这时我们就会形成一个”先来后到”的队列,对这5个客户端的连接请求,一个一个进行处理。比如说,我们设置监听队列大小为5,表示队列最多容纳5个客户端等待处理。但有10个客户端同时发起了连接请求,这时,服务端会先处理第1个发起请求的那个客户端,前2~6个客户端则在等待队列中,最晚发起请求的4个客户端,则会被拒绝。

  1. 等待处理客户端连接请求
connfd,addr = sockfd.accept()      
功能: 阻塞等待处理客户端请求(等待客户端连接,啥时候有客户端连接,才结束阻塞)
返回值:
   connfd : 客户端连接套接字.
   客户端连接后,新产生的专门与客户端进行通信的套接字对象。
   则每当有一个客户端连接时,都会有一个新的专用于连接的套接字对象产生。
   就像对每个客户端提供一个专属管家一样,提升用户端体验。
   我们如果找到一个连接套接字,就可以知道它对应的客户端是谁
   addr :连接的客户端地址      

阻塞函数:当程序运行到这个函数时,则程序暂停执行。程序在等待某种条件,当条件满足后才继续执行,如:input()、sleep()。阻塞函数在IO操作中很常见。

  1. 消息收发
data = connfd.recv(buffersize)      
功能 : 接受客户端消息
参数 :每次最多接收消息的大小(最多一次接受多少字节的消息)
返回值: 接收到的内容      
n = connfd.send(data)      
功能 : 发送消息
参数 :要发送的内容-bytes格式(字节串格式)
返回值: 发送的字节数      

备注1:所有和网络相关的消息传送都得是bytes格式(字节串格式).发送和接收都得是字节串。

备注2:我们发送/接收的是字节串,但平时写/读的是字符串,所以我们需要用encode()/dencode()进行转换。

  1. 关闭套接字
sockfd.close()      
功能:关闭套接字      
举个例子

服务端代码:

import socket

#创建流式套接字
sockfd = socket.socket(socket.AF_INET, \
                       socket.SOCK_STREAM)
#绑定地址
sockfd.bind(('127.0.0.1', 8888))

#设置监听
sockfd.listen(5)

#等待处理客户端链接
print("Waiting for connect....")
connfd, addr = sockfd.accept()

#收发消息
data = connfd.recv(1024)
print("接收到的消息:", data.decode())

n = connfd.send(b'Receive your message')
print("发送了 %d 个字节数据" % n)

#关闭套接字
connfd.close()
sockfd.close()      

我们运行一下,得到以下结果:

网络编程(part9)--socket套接字编程之TCP套接字

结果说明,程序阻塞在accept,等待客户端连接。所以这时,我们就要找一个客户端与之连接,下一节,我们就学一下客户端流程。

客户端流程

先看看流程图:

网络编程(part9)--socket套接字编程之TCP套接字

代码实现

  1. 创建套接字(和客户端代码相同)

    注意:只有相同类型的套接字才能进行通信

  2. 请求连接
sockfd.connect(server_addr)      
功能:连接服务器
参数:元组  服务器地址      
  1. 收发消息(和客户端代码相同)

    注意: 为了防止两端都阻塞,故recv和send要配合使用。比如,服务端是先send后recv,那么客户端则需要先recv后send。否则,若两端同时recv,则两端都会阻塞。

  2. 关闭套接字(和客户端代码相同)
举个例子

客户端代码:

网络编程(part9)--socket套接字编程之TCP套接字

服务端代码:

网络编程(part9)--socket套接字编程之TCP套接字

我们想要运行,但是若先运行客户端,则会报错。所以我们要先启动服务端

①运行服务端

查看服务端的控制台Console 2/A输出的结果:

网络编程(part9)--socket套接字编程之TCP套接字

②运行客户端

我们先看一下服务端的控制台Console 2/A输出的结果:

网络编程(part9)--socket套接字编程之TCP套接字

我们再看一下客户端的控制台Console 3/A输出的结果:

网络编程(part9)--socket套接字编程之TCP套接字

消息收发成功了,很好~

TCP套接字数据传输特点

①TCP连接中,当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。

②TCP连接中,如果一端已经不存在,仍然试图通过send发送信息,则会产生BrokenPipeError

③一个监听套接字可以同时连接多个客户端,也能够重复被连接。

做个练习

要求1:一个客户端退出了,服务器不会退出,而是连接下一个客户端

要求2:客户端可以不停的循环发送消息

服务端代码:

#-*- coding: utf-8 -*-

import socket

#创建流式套接字
sockfd = socket.socket(socket.AF_INET, \
                       socket.SOCK_STREAM)

#绑定地址
sockfd.bind(('127.0.0.1', 8888))

#设置监听
sockfd.listen(5)

#等待处理客户端链接
while True:
    print("Waiting for connect....")
    try:
        connfd, addr = sockfd.accept()
        print("Connect from:", addr)
    except KeyboardInterrupt:
        print("退出服务")
        break

    # 收发消息
    while True:
        data = connfd.recv(1024)
        # 得到空则退出循环
        if not data:
            break
        print("接收到的消息:", data.decode())
        n = connfd.send(b'Receive your message')
        print("发送了 %d 个字节数据" % n)
    connfd.close()

#关闭套接字
sockfd.close()      

客户端代码:

#-*- coding: utf-8 -*-

from socket import *

#创建tcp套接字
sockfd = socket()

#发起连接
server_addr = ('127.0.0.1',8888)
sockfd.connect(server_addr)

#收发消息
while True:
    data = input("消息:")
    if not data:
        break
    sockfd.send(data.encode())
    data = sockfd.recv(1024)
    print("From server:",data.decode())

#关闭
sockfd.close()      

先运行服务端,再运行客户端并发送消息。

客户端结果:

网络编程(part9)--socket套接字编程之TCP套接字

服务端结果:

网络编程(part9)--socket套接字编程之TCP套接字

网络收发缓冲区

①网络缓冲区有效的协调了消息的收发速度、缓解收发压力。

②send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。

网络缓冲区完成消息传递示意图:

网络编程(part9)--socket套接字编程之TCP套接字
举个例子

还记得我们上面那个练习么?其中一段代码是:

网络编程(part9)--socket套接字编程之TCP套接字

它表示,每次最多可接收消息大小为1024个字节.

我们来更改一下最大可接受的消息字节数为5:

网络编程(part9)--socket套接字编程之TCP套接字

用客户端发送消息:

网络编程(part9)--socket套接字编程之TCP套接字

服务端结果:

网络编程(part9)--socket套接字编程之TCP套接字

客户端结果:

网络编程(part9)--socket套接字编程之TCP套接字

则说明,客户端1次发送,服务端分3次接收了,同时返回给服务端3句’ Receive your message’也就是说,服务端的内层循环(如下图所示)循环了3次

网络编程(part9)--socket套接字编程之TCP套接字

备注1:所以我们一开始说【函数recv()是阻塞函数,当发送方不发消息,就会阻塞】。但其实更准确的来说,并不是发送方不发消息就会阻塞,接收方其实是先从缓冲区去拿消息,当缓冲区为空时,会阻塞,当缓冲区一直有消息时,则会一直获取消息,一直不阻塞。

注意! 运行上面的代码时,客户端有时候也会出现以下这种情况:

网络编程(part9)--socket套接字编程之TCP套接字

TCP粘包

  • 原因

    TCP以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。

  • 影响(分情况,比如:传一部电影/发送用户名消息)

    ①如果发送的内容中,每个信息都有独立的含义(比如:发送几个用户姓名),需要接收端独立解析,此时,这时粘包会有影响。

    ②如果发送的是一个字节流文件,是一个连在一起的整体(比如:发送一部电影),接收端无需单独解析,而是将所有接收到内容最终合成一个整体,这时粘包没啥影响。

  • 处理方法

    ①人为的添加消息边界,比如:每次发送一个姓名时,在姓名后加一个特殊符号。

    ②控制发送速度(因为粘包的产生是因为收发速度不协调),比如用sleep()函数调节。