基于TCP协议的套接字网络(socket)编程

基于TCP协议的套接字网络(socket)编程

一、什么是socket

1、介绍:

在了解了osI七层协议之后,我们看到,应用层与传输层之间,有着一个socket的抽象层,这里的抽象层并不存在于osI七层协议之中,这里的socket抽象层是为应用层通过下面所有层次以后再通过网络通信的一种接口

121-基于TCP协议的套接字编程-socket层.jpg?x-oss-process=style/watermark

2、什么是socket?

如图所示:

  • Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
  • 所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

3、套接字socket封装的好处:

- 对应用程序的开发者来说,只需要关注应用中相关的事情,应用层你想怎么处理封装你的数据, 在应用层定制什么样的协议, 你想干什么就干什么,只需要把应用层处理好的数据交给socket,它会帮你处理到传输层,到网络层, 到数据链路层, 到物理层所需要干的事情.

4、研究套接字socket抽象层次的目的是什么?

- socket抽象层不是去帮你写应用程序的,主要是把你应用程序所写好的数据基于网络协议发出去

5、注意:

  • 注意:也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序,而程序的pid是同一台机器上不同进程或者线程的标识。

二、套接字发展史及分类

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

1、基于文件类型的套接字家族

套接字家族的名字:AF_UNIX
  • unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

2、基于网络类型的套接字家族

套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

3、传输协议类型

流式套接字 : SOCK_STREAM
  • 流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议
数据报套接字:SOCK_DGRAM
  • 数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理
原始套接字:SOCK_RAW
  • 原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接

4、总结

早期套接字并不是用来网络通信的,一般是用来处理一台计算机之上两个进程之间的互相通信

我们知道计算机开启两个进程,申请的内存的空间,两个进程的内存空间是互相隔离的 ,并且是物理隔离,为的就是保证数据的安全,那么进程与进程之间的数据是得不到交互的

但是硬盘是所有程序公用的, 于是就可以把一个进程的数据先写到硬盘中去, 让另一个进程从硬盘中取, 这样就实现了进程与进程之间的数据交互

三、套接字的工作流程

1099775-20180729172708689-449832714

客户端与服务端之间的连接过程主要可以分为四个步骤,如下:

1、服务器绑定IP+Port并建立监听

  • 客户端初始化Socket动态库后创建套接字,然后指定客户端Socket的地址,循环绑定Socket直至成功,然后开始建立监听,此时客户端处于等待状态,实时监控网络状态

2、客户端向服务端发送请求

  • 客户端的Socket向服务器端提出连接请求,此时客户端描述出它所要连接的Socket,指出要连接的Socket的相关属性,然后向服务器端Socket提出请求

3、连接请求确认并建立

  • 当服务器端套接字监听到来自客户端的连接请求之后,立即响应请求并建立一个新进程,然后将服务器端的套接字的描述反馈给客户端,由客户端确认之后连接就建立成功

4、连接建立成功之后进行数据收发

  • 然后客户端和服务器两端之间可以相互通信,传输数据,此时服务器端的套接字继续等待监听来自其他客户端的请求

5、总结:

客户端:
socket()
    客户端所在的应用层拿到抽象层中的socket指定直接打交道的下一层的传输层的协议.(这里以TCP/UPD为例) 
connect()      
    接着通过拿到TCP服务端的IP和端口,找到服务端主机上与之通信的那个独一无二的程序.(提示:客户端不需要绑定,客户端的端口,在访问服务端时服务端就能拿到客户端的端口,因此客户端端口不需要绑定)
read()和write()
    与服务端链接成功以后,就可以对服务端进行收发数据操作.
close()
    最后, 向操作系统发送系统调用关闭客户端与服务端之间的连接通路,并回收建立连接时所占用的系统资源,
服务端:
socket()
    服务端指定socket与传输层打交道的协议下一层的传输层的协议.(这里以TCP/UPD为例)
bind()       
    服务端需要绑定Ip和端口,等客户端通过这个Ip和端口, 可以找到全世界独一无二的在这个服务器上的这个程序, 并与之通信.
listen()
    三次握手建立链接之前,服务端本身就处于LISHEN状态,此时服务端的链接还在半连接池中,还没有被客户端所进行链接
accept()
    它其实就是Tcp建立链接三次握手的地方,如果有请求,则会去listen的半连接池中取获取连接. 它是基于串联的一个一个的服务,一个服务结束了以后才能进行下一个服务
read()和write()
    客户端建立成功以后, 然后服务端就可以对客户端进行收发数据.
close()
    最后, 向操作系统发送系统调用关闭服务端与客户端之间的连接通路,并回收建立连接时所占用的系统资源,    

6、Python代码实现

import socket

# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0
socket.socket(socket_family, socket_type, protocal=0)

# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能大幅减短我们的代码
tcpSock = socket(AF_INET, SOCK_STREAM)

四、socket模块的常用函数(方法)

1、socket的实例化(得到一个套接字对象)

  • 格式:socket(family, type, protocal=0)
  • 三个参数:常用协议组、socket类型,指定的协议(默认0)
    • AF_INET(默认值)、AF_INET6、AF_UNIX、AF_ROUTE等
    • SOCK_STREAM(TCP类型)(默认值)、SOCK_DGRAM(UDP类型)、SOCK_RAW(原始类型)
    • 通常不写,默认为‘0“,使用默认的协议和类型
s=socket.socket()  # 等同于下面
socket.socket(socket.AF_INET,socket.SOCK_STREAM)   # 等同于上面 
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)  # 如果想用UDP就得这样写

2、服务端套接字函数

函数说明
s.bind()绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址
s.listen()开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量,该值至少为1,一般为5就够用了
s.accept()被动接受TCP客户端连接,(阻塞式)等待连接的到来

3、客户端套接字函数

函数说明
s.connect()主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误
s.connect_ex()connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

4、公共用途的套接字函数

函数说明
s.recv()接收TCP数据,数据以字符串形式返回,缓冲区(bufsize)指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略
s.send()发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小(提示: 在输入空的时候小于)
s.sendall()完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常
s.recvfrom()接收UDP数据,与recv()类似,但返回值是(data_bytes,address)。其中data_bytes是接收的bytes类型的数据,address是发送数据的地址, 以元组(‘IP’, port)形式表示
s.sendto()发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数
s.close()关闭套接字
s.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
s.getsockname()返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value)设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen])返回套接字选项的值。

5、面向锁的套接字方法

函数说明
s.setblocking(flag)如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值),非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
s.settimeout(timeout)设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None

6、面向文件的套接字函数

函数说明
s.fileno()返回套接字的文件描述符
s.makefile()创建一个与该套接字相关连的文件

五、基于TCP实现的套接字

注意:TCP 基于连接通信的, 所以必须先启动服务端, 在启动客户端

1.模拟打电话简单通信连接

  • TCP服务端
"""
from socket import *
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    第一个参数: 指定套接字家族中的工作类型. 
        AF_INET: 指定基于网络通信的套接字类型.(补充: AF全称Address Family)
        
    第二个参数: 指定的是让socket这个抽象层与下一层传输层所打交道的协议类型.
        SOCK_STREAM: 这是代指的是一种流式协议,流式协议指的就是TCP协议.
    
phone.bind(('IP', PORT))   
    1) 第1个参数指定IP,字符串格式.
    2) 第2个参数指定端口,(端口范围0~65535. 其中0~1023都是公认端口, 也叫熟知端口. 最好不要使用, 不然会端口冲突.)    
    补充: 
        - 服务端绑定的Ip和端口, 其中的Ip地址是自己局域网中的Ip地址. 要想被其他局域网中的主机进行访问,那么得在公网申请一个地址,把公网地址与本服务器的地址绑定一种映射关系. 
        - 0.0.0.0: 这种IP地址能被任何的机器所访问到, 对本机来说,它就是一个“收容所”,所有不认识的“三无”人员,一律送进去。(详细网址: https://baike.baidu.com/item/0.0.0.0/7900621?fr=aladdin)
        - 127.0.0.1: 这种地址是回环测试地址,这个地址只能自己本机访问, 就算在同一个局域网中另一台主机也访问不到.
        - 在bind操作之前有一种命令setsockopt可以,重新利用你的端口(用于测试)
        
phone.listen(5)
    注意!!! 这里获取的是连接请求, 不是建立成功以后的连接.
    1) 这里参数指定半连接池的大小(int类型). 我这里指定的是5, 这里可以被6个客户端进行连接请求, 当第7个客户端进行连接所就不能成功获取半连接池中的链接请求. 
    2) 意义: 使用这种半连接池,可以合理的去控制你服务端被客户端所访问连接请求的个数,进而控制客户端对服务端资源的访问, 防止服务端被客户端访问资源过多,造成服务端撑死.
    3) 表现: 目前这种情况,当有一个客户端以及链接成功, 服务端在为这个客户端提供服务. 如果有5个客户端发起链接请求,那么这5个客户端,会进行等待(因为这里没有实现并发),等待上一个客户端断开, 好与服务端的accept建立连接. (注意!!: 服务端正在为客户端提供通讯循环的那个客户端,也算作一次链接请求,所以一共是6次,这里的半连接池只能支持6个客户端发起连接请求.)
    
conn, client_addr = phone.accept()
    1) accept: 这里是等待客户端发送syn链接请求, 没有客户端进行链接,这里会堵塞住, 处于一种等待的状态.
    2) conn: 这是一个属于客户端的套接字对象,通过拿到这个套间之对象与客户端进行通信.
    3) client_addr: 返回值是一个元组形式
        - 元组中第1个参数,是服务端与客户端建立双向链接通路的一个对象.
        - 元组中第2个参数, 是拿到客户端的Ip和端口,它们两个也是在包含在一个人组中.
    注意: 如果客户端和服务端都在同一台主机之上测试运行,服务端拿到的client_addr中的Ip和端口. 其中的端口和服务端的端口一定不一样,因为同一台主机之上的端口是独一无二的.
    
data = conn.recv(1024): 这里指定最大接收的数据量是1024个字节(Bytes). 返回的是一个bytes类型. (注意: 这个数不能超过内存的字节接收大小,数据的处理都是在内存之中中转的,所以不能超过.)
        
conn.send(bytes类型的数据): 这里指定发送bytes类型的数据, 往conn这个通向客户端的连接通路中发送数据.          

conn.close(): 当客户端发送FIN结束请求时,这里要关闭该客户端的链接通路, 同时也回收占用在服务端上之前申请的与客户端的系统的连接资源.(必选操作)

phone.close(): 这里是关闭服务端的系统进程(可选操作: 因为服务器不应该被关闭,应该要一直在运行.)
    补充: 这里关闭了服务端,绑定的IP和端口如果关闭了以后,操作系统需要回收这个资源,回收这个资源需要一定的时间,所以如果需要再次运行上面的bind(), 则端口需要重新指定,让操作系统为上一次指定的端口申请的系统资源的回收腾出时间.
"""
import socket

🧇1.买手机(创建socket对象)
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 第一个参数,是基于网络套接字,  第二个是种类:tcp称为流式协议,udp称为数据报协议(SOCK_DGRAM)


🍛2.插卡/绑卡(绑定地址和端口号)
phone.bind(('127.0.0.1', 8060)) 
# 一个元组,这里绑定的是一个本机回环地址,只能本机和本机通信,一般用于测试阶段


🦪3.开机 (处于监听状态)
phone.listen(5)  
# 5 是设置的半连接池,限制的是请求数,只能同时与一台客户机通信,其它的挂起

🥘4.等待电话连接(等待客户机的请求连接)
print('等待连接...')
conn, client_addr = phone.accept()  
# (conn:三次握手建立的链接,client_addr:客户端的 IP 和端口)

print(conn)
print(client_addr)

🍟5.开始通信:收/发消息
data = conn.recv(1024)    # 最大接受的字节数1024
print('来自客户端的数据:', data)
conn.send(data.upper())   # 返回数据

🍿6.挂掉电话连接 (关闭TCP连接)
conn.close()

🍟7.关机 (关闭服务器)
phone.close()
  • TCP客户端
"""
from socket import *
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('IP', PORT)): 这里指定的是你需要去连的服务端的IP和端口.

phone.send(bytes类型数据): 这里要指定bytes类型数据,往双向通路中的管道 服务端1 --> 客户端1 的管道中传输数据. 

data = phone.recv(1024) 

phone.close(): 这里是客户端向服务端发送FIN结束请求, 同时也回收占用在客户端上系统的连接资源.(必选操作)
"""

import socket

🍟1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(phone)


🍟2.拨电话 (客户端地址会变,不需要绑定地址)
phone.connect(('127.0.0.1', 8060))
# 指定服务器 IP 和端口,一个元组,这里是客户机与服务端建立连接

🍟3.开始通信:发/收消息, 通信循环
while True: 
    msg = input('请输入消息>>').strip()
    if len(msg) == 0:continue #这里解决的是客户端发送空数据, 而客户端操作系统缓存中并没有数据发送给服务端, 因而服务端并不能从服务端的操作系统缓存中获取数,因此服务端和客户端同时处在一个recv堵塞的状态,
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)  # 最大接受字节数1024
    print(data.decode("utf-8"))

🍟4.关闭客户端
phone.close() 

2、问题:重启服务端报错 Address already in uer (端口被占用) 解决方法

🎂在bind()函数之前加上一条“socket”配置
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)  # 表示重用 ip 和 端口
🍖tcp是按一个简单聊天程序

六、应用实例

用TCP是实现一个简单的聊天程序

需求:实现一个简单聊天程序,一来一回,并且一个连接断开才可以连接下一个。

  • 服务端
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(("127.0.0.1",8088))
phone.listen(5)
while True:
    print("start recv...")
    conn,client_addr=phone.accept()
    print(f"连接了一个客户端{client_addr}")
    while True:
        try:
            data=conn.recv(1024)
            if len(data)== 0:break
            print(f'来自客户的数据:\033[1;30;46m{data.decode("utf-8")}\033[0m')
            user=input("请输入发送内容(按q退出,enter发送)>>>:")
            if user.lower()=="q":break
            conn.send(user.encode("utf-8"))

        except ConnectionError:
            break
    conn.close()


  • 客户端
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",8088))
while True:
    msg=input("请输入发送内容(按q退出)》》》:").strip()
    if msg.lower()=="q":break
    if len(msg)==0 :continue
    phone.send(msg.encode("utf-8"))
    data=phone.recv(1024)
    print(f"来自女朋友的语音消息: \033[1;30;46m{data.decode('utf-8')}\033[0m")
phone.close()

七、总结

1、引发阻塞操作是哪几种操作?
  • accept, recv, send: 其中明显引发阻塞操作的是accept, recv.
2、服务端需要处理客户端发送空数据的问题.(引发原因: 因为send可以发空, 而recv不能收空.)
  • 引发问题:如果客户端发空, 服务端就一直在堵塞在原地,而这个时候客户端,也收不到服务端所发过来的数据,也同时堵塞在原地.
  • 解决: 客户端要进行判断,让客户端输入内容不能为空
  • 低层原理:客户端的send操作与服务端的recv操作并不是一对应的. 客户端与服务端的收发都是针对自己.
3、客户端强行终止连接, 服务端会出现异常. 如果是在windows系统中,会抛出异常(ConnectionAbortedError). 如果是在linux系统中recv会一直接收空,从而进入死循环.
  • 本来客户端与服务端是一个正常的链接,客户端没有和服务端进行商量,客户端强行断开链接,而服务端的conn这个套接字对象以为客户端的失效的链接还是正常的,还是在基于一个正常的行为去接收客户端的数据.
  • 举个例子: 基于TCP协议通信的客户端与服务端之间建立连接以后两者之间了搭建了一个桥墩子,如果客户端把桥墩子炸了,服务端也就炸了, 所以我们要提供解决的办法, 让服务器不受影响.

  • 解决linux系统中的异常: 在服务端中判断接收的数据是否为空.(注意: 这里的空并不是客户端发送空数据服务端所接收到的空,而是客户端强行终止链接以后,linux系统中引发的一种错误,)

  • 解决window系统中的异常: 使用异常处理机制.

    补充!!!: 还有另一种情况就是客户端正常使用close()断开连接, 在windows系统中的服务端也会收到空的数据.

4、解决客户端强行终止链接以后, 让服务端已处于一直提供服务的状态.
  • 问题:这种通信循环外面套外循环的这种链接循环只是一种,1对1的服务,不能分心,只有等上一个链接的客户端,断开以后,下一个链接的客户的连接才能建立.
  • 举个例子: 连接循环就像一个,拉客的,通信循环是接客的,拉客只需要有一个就行了,而我们的接客的,要需要很多个,才能为很多的客户端进行服务,这个时候我们就需要用到并发.
  • 解决: 在客户端的通信循环的基础之上,在外面再套一层接收客户端链接的while循环, 达到链接循环的目的.
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

贾维斯Echo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值