网络编程 - Socket套接字,基于TCP的套接字程序,基于UDP的套接字程序,TCP协议与UDP协议的区别

Socket套接字编程

  • 为何学习socket套接字一定要先学习互联网协议:

    • 1、首先:要想开发一款自己的C/S架构软件,就必须掌握socket编程

    • 2、其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的

    • 3、然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。

socket层

从上图中,我们没有看到socket的影子,它在哪里呢?用图说话一目了然

socket套接字是什么

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

所以,无需深入理解tcp/udp协议,socket已经封装好了,只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

  • 基于文件类型的套接字家族的名字:AF_UNIX

  • 基于网络类型的套接字家族的名字:AF_INET

套接字工作流程

  • 基于TCP套接字通信的流程图:

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

socket()模块的用法:

import socket
socket.socket(socket_family,socket_type,protocal=0)
"""
socket_family 可以是 AF_UNIX 或 AF_INET。
socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 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、服务端套接字函数
s.bind()    # 绑定(主机,端口号)到套接字
s.listen()  # 开始TCP监听
s.accept()  # 被动接受TCP客户的连接,(阻塞式)等待连接的到来

2、客户端套接字函数
s.connect()     # 主动初始化TCP服务器连接
s.connect_ex()  # connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

3、公共用途的套接字函数
s.recv()         # 接收TCP数据
s.send()         # 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall()      # 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
					
s.recvfrom()     # 接收UDP数据
s.sendto()       # 发送UDP数据
s.getpeername()  # 连接到当前套接字的远端的地址
s.getsockname()  # 当前套接字的地址
s.getsockopt()   # 返回指定套接字的参数
s.setsockopt()   # 设置指定套接字的参数
s.close()        # 关闭套接字

4、面向锁的套接字方法
s.setblocking()  # 设置套接字的阻塞与非阻塞模式
s.settimeout()   # 设置阻塞套接字操作的超时时间
s.gettimeout()   # 得到阻塞套接字操作的超时时间

5、面向文件的套接字的函数
s.fileno()      # 套接字的文件描述符
s.makefile()    # 创建一个与该套接字相关的文件

基于TCP的套接字程序

tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

tcp服务端

server = socket()  # 创建服务器套接字
server.bind()  # 把地址绑定到套接字
server.listen()  # 监听链接
inf_loop:  # 服务器无限循环
    cs = server.accept()  # 接受客户端链接
    comm_loop:  # 通讯循环
        cs.recv()/cs.send()  # 对话(接收与发送)
    cs.close()  # 关闭客户端套接字
server.close()  # 关闭服务器套接字(可选)

tcp客户端

client = socket()  # 创建客户套接字
client.connect()  # 尝试连接服务器
 comm_loop:   # 通讯循环
     client.send()/client.recv()  # 对话(发送/接收)
client.close()   # 关闭客户套接字

基于TCP协议的套接字通信

socket通信流程与打电话流程类似,以打电话为例来实现一个简版的套接字通信
## 服务端.py文件

import socket
# 1、买手机
# server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式协议
server = socket.socket()  # 默认就是基于网络的TCP传输协议,同上

# 2、绑定电话卡(绑定ip和port)
server.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、开机(过渡)
server.listen(5)  # 半连接池
 
# 4、接收链接请求(监听)
conn, client_addr = server.accept()  # server.accept背后在做tcp的三次握手,有返回值
# 返回值赋值给conn对象(代表tcp三次握手的封装成果)和client_addr(客户端的ip + port(端口))
                                
# 5、接收客户端发送的消息(听别人说话)
data = conn.recv(1024)  # 最大接收的字节数,data(bytes类型)接收的是网络发过来的数据
print(data)

# 6、给客户端回复消息(给别人回话)
conn.send(data.upper())  # 以大写格式回复消息

# 7、挂电话
conn.close()

# 8、关机
server.close()



## 客户端.py文件

import socket
# 1、买手机
client = socket.socket()

# 2、打电话(拨号)
client.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、发\收数据
client.send("hello".encode('utf-8'))  # 发消息(只能发送字节类型)

data = client.recv(1024)  # 收消息

print(data.decode('utf-8'))  # 服务端发来的数据,收到是大写的bytes,用utf-8解码

# 4、关机
client.close()

'''
补充知识:回环地址IP
IP地址127.0.0.1 代表本机IP地址,等价于localhost⽤ http://127.0.0.1 就可以测试本机中配置的Web服务器。
    
127.0.0.1,通常被称为本地回环地址(Loop back address),不属于任何一个有类别地址类。它代表设备的本地虚拟接
口,所以默认被看作是永远不会宕掉的接口。在windows操作系统中也有相似的定义,所以通常在不安装网卡前就可以ping通
这个本地回环地址。一般都会用来检查本地网络协议、基本数据接口等是否正常的
'''

通信循环及代码优化

1.客户端校验消息不能为空
2.服务端添加兼容性代码(mac linux)
3.服务端重启频繁报端口占用错误
'''
在重启服务端时可能会遇到

OSError:[Errno 48] Address already in use  # 地址已经在使用中

这个是由于服务端仍然存在四次挥手的time_wait状态在占用地址
# 在绑定之前加入一条socket配置,重用ip和端口

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)  #就是它,在bind前加
server.bind(('127.0.0.1',8080))

'''
4.客户端异常关闭服务端报错的问题
    异常捕获
  
5.服务端链接循环

6.半连接池
    设置可以等待的客户端数量

加上通信循环与链接循环,即异常处理

## 服务端.py文件

import socket
# 1、买手机
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式协议
# server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  关于端口占用的修改

# 2、绑定电话卡
server.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、开机
server.listen(5)  # 半连接池 

# 4、接收链接请求 
while True:  # 链接循环,可以不停的接电话
    conn, client_addr = server.accept()
    print(client_addr)     
# 5、收发消息 
    while True:  # 通信循环,可以不断的通信,收发消息
        try:  # 应对Windows系统的异常
            data = conn.recv(1024)
            if len(data) == 0:  # 应对linux系统的异常处理,正常情况data不会等于0
                break
            print(data)
            conn.send(data.upper()) 
	except Exception:  # 应对Windows系统的异常
            break  # 无论应对哪个异常都应该将循环结束掉
      
# 6、挂电话
	conn.close()
  
# 7、关机
server.close()


## 客户端.py文件

import socket
# 1、买手机
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式协议

# 2、打电话
client.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、发\收数据
while True:  # 加while循环
    msg = input('>>>>:').strip()
    if len(msg) == 0:  # 解决输入空的bug
        continue
    client.send(msg.encode('utf-8'))  # 发消息
    data = client.recv(1024)  # 收消息
    print(data.decode('utf-8'))  
    
# 4、关闭
client.close()

基于UDP协议的套接字程序

  • udp是无链接的,先启动哪一端都不会报错

udp服务端

server = socket()  # 创建一个服务器的套接字
server.bind()  # 绑定服务器套接字
while True:  # 服务器无限循环
    client = server.recvfrom()/server.sendto()  # 对话(接收与发送)
server.close() # 关闭服务器套接字

udp客户端

client = socket()  # 创建客户套接字
while True:  # 通讯循环
    client.sendto()/client.recvfrom()  # 对话(发送/接收)
client.close()  # 关闭客户套接字

基于UDP协议的套接字通信

# 服务端.py文件

from socket import *

server = socket(AF_INET,SOCK_DGRAM) # 数据报协议

server.bind(('127.0.0.1',8081))

while True :
    data,client_addr = server.recvfrom(1024)
    server.sendto(data.upper(),client_addr)

server.close()


# 客户端.py文件  (支持并发但比较狭窄)

from socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True :
    msg = input('请输入您要发送的消息>>>:').strip()
    client.sendto(msg.encode('utf-8'),('127.0.0.1',8081))
    data,server_addr = client.recvfrom(1024)
    print(data.decode('utf-8'))

client.close()


TCP协议与UDP协议的区别

  • 1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

  • 2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

  • 3.TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。


UDP的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

TCP的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

posted @ 2021-01-15 21:36  山风有耳  阅读(205)  评论(0编辑  收藏  举报