网络编程-套接字socket模块

套接字发展史及分类

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

TCP协议的套接字

面向连接的套接字,即在通信前建立一条连接(TCP的三次握手和四次挥手),这种通信方式也被称为"虚电路"或"流套接字"。

 
面向连接的通信方式提供了顺序的,可靠地,不会重复的数据传输,而且也不会被加上数据边界。
 
这个也意味着每一个要发送的信息,可能会被拆分成多分,每一份都会不多不少的正确到达目的地,然后被重新按顺序拼装起来,传给正在等待的应用程序
 
实现这种连接的主要协议就是传输控制协议(即TCP)。
要创建TCP套接字就要在创建的时候指定套接字类型为SOCK_STREAM
1 ss = socket() #创建服务器套接字
2 ss.bind()      #把地址绑定到套接字
3 ss.listen()      #监听链接
4 inf_loop:      #服务器无限循环
5     cs = ss.accept() #接受客户端链接
6     comm_loop:         #通讯循环
7         cs.recv()/cs.send() #对话(接收与发送)
8     cs.close()    #关闭客户端套接字
9 ss.close()        #关闭服务器套接字(可选)
tcp服务端
1 cs = socket()    # 创建客户套接字
2 cs.connect()    # 尝试连接服务器
3 comm_loop:        # 通讯循环
4     cs.send()/cs.recv()    # 对话(发送/接收)
5 cs.close()            # 关闭客户套接字
tcp客户端
假如端口被socket使用过,并且利用socket.close()来关闭连接,但此时端口还没有释放,要经过一个TIME_WAIT的过程之后才能使用,这是TNN的相当烦银的,为了实现端口的马上复用,可以选择setsockopt()函数来达到目的。(以下是网上找到的一篇文章的一小段相关例子,试用之后,相当有效果,特此提取出来收藏)

端口复用的实现,我在这里用Python举个TCP端口复用的例子,UDP套接字要做的完全一样。
import socket
tcp1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#在绑定前调用setsockopt让套接字允许地址重用
tcp1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)#接下来两个套接字都也可以绑定到同一个端口上
复用端口,解决端口被占用

TCP实例-远程执行命令

import subprocess
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(("127.0.0.1",8888))
server.listen(5)
while True:
    conn,addr = server.accept()
    while True:
        try:
            cmd = conn.recv(1024)
            # if not cmd:break #  针对Linux服务端,当客户端断开连接时,服务器一直接到空内容
            cmd = cmd.decode("utf-8")
            obj = subprocess.Popen(cmd,shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )
            stdout = obj.stdout.read()
            stderr = obj.stdout.read()
            conn.send(stdout+stderr)
        except ConnectionResetError:
            break
tcp服务端
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8888))

while True:
    cmd = input(">>>").strip()
    if not cmd:continue
    client.send(cmd.encode("utf-8"))

    data = client.recv(1024)
    print(data.decode("gbk"))

client.close()
tgp客户端
 当服务端发送消息过长,超过了客户端接受的最大长度时,就会产生粘包现象
 

粘包问题的产生

    套接字是从操作系统中收发数据,然后操作系统包ip头和以太网头
          收发数据都是对操作系统的缓冲区进行的操作

由接收方造成的粘包

  当接收方不能及时接收缓冲区的包,造成多个包接收就产生了粘包
       客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次
       遗留的数据

由传输方造成的粘包

  tcp协议中会使用Nagle算法来优化数据。发送时间间隔短,数据量小的包会一起发送,造成粘包

结局粘包问题的基本套路

     粘包问题的根源在于,接收端不知道发送端将要传送的字节流长度,所以解决粘包的方法就是围绕
     如何让发送端在发送数据前把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循
     环接受完所有数据
 
  方法:自定义一个报头,传输数据的大小,这个报头是固定字节,在发送数据前发送
import subprocess
import struct
from socket import *
server = socket(AF_INET,SOCK_STREAM) # 创建套接字对象
server.bind(("127.0.0.1",8888))
server.listen(5)
while True:
    conn,addr = server.accept() # 等待连接,返回的第一个参数是套接字对象,第二个参数是客户端信息
    while True:
        try:
            cmd = conn.recv(1024)
            cmd = cmd.decode("utf-8")
            obj = subprocess.Popen(cmd, shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   ) # 调用模块,执行命令。并且收集命令执行的结果
            stdout = obj.stdout.read() # 把正确结果读出,存储在变量里
            stderr = obj.stdout.read() # 把正确结果读出,存储在变量里
            # 第一步:制作报头:
            total_size = len(stdout)+len(stderr)
            header = struct.pack("i",total_size) #存储包含数据长度的固定字节的字节码
            # 第二步:先发报头:
            conn.send(header)
            # 第三步:发送命令结果
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
server.close()
初级解决粘包问题-服务端
import struct
from socket import *

client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8888))

while True:
    cmd = input(">>>").strip()
    client.send(cmd.encode("utf-8"))

    # 第一步:收到报头
    header = client.recv(4) # 已知报头长度为四个字节
    total_size = struct.unpack("i",header)[0] # 拿到数据长度

    # 第二步:收完整真实数据
    recv_size = 0
    res = b""
    while recv_size < total_size:     # 计数器小于报头传入的数据长度
        recv_data = client.recv(1024) # 从操作系统拿数据
        res += recv_data              # 拼接到res
        recv_size += len(recv_data)   # 计数器加上拿到的数据的长度
    print(res.decode("gbk"))
client.close()
初级解决粘包问题-客户端

初级解决粘包问题的弊端

一 能够表示的大小有限。struct.pack能够转化的数字长度有限,不能够解决某些问题,比如下载一个大文件
二 有时报头中还需要传递其他信息,比如对于文件的描述
 
基于此类问题,我们会创建一个字典,其中存储了关于文件的各类信息
import subprocess
import struct # 完成报头中的数字转换为固定长度的字节码
import json # 序列化模块 将数据类型信息转化为字符串
from socket import * # 套接字模块

server = socket(AF_INET,SOCK_STREAM)
server.bind(("127.0.0.1",8888))
server.listen(5)
while True:
    conn,addr = server.accept()
    while True:
        try:
            cmd = conn.recv(1024)
            cmd = cmd.decode("utf-8")
            obj = subprocess.Popen(cmd,shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            # 制作报头,报头里放数据大小,MD5,文件名
            header_dict ={
                "total_size":len(stdout)+len(stderr),
                "md5":"********",
                "filename":"文件名"
            }
            header_json = json.dumps(header_dict) # 序列化报头
            header_bytes = header_json.encode("utf-8") # 将报头转码编程字节码
            header_size = struct.pack("i",len(header_bytes)) # 将转码后的报头长度转换成固定长度的字节码

            # 先发报头长度
            conn.send(header_size)

            # 再发报头
            conn.send(header_bytes)

            # 在发送真实数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
server.close()
终极解决方法-服务端
import struct
import json
from socket import *

client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8888))

while True:
    cmd = input(">>>").strip()
    if not cmd:continue
    client.send(cmd.encode("utf-8"))

    #  先收报头长度-已知长度
    obj = client.recv(4)
    header_size = struct.unpack("i",obj)[0] #  得到了报头长度

    #  接收报头,解出报头内容
    header_bytes = client.recv(header_size) # 根据报头长度得到报头
    header_json = header_bytes.decode('utf-8') # 报头转码成str
    header_dic = json.loads(header_json) # 反序列化的到字典

    total_size = header_dic['total_size'] # 得到真实数据长度

    # 3:循环收完整数据
    recv_size = 0
    res = b""
    while recv_size < total_size:  # 计数器小于报头传入的数据长度
        recv_data = client.recv(1024)  # 从操作系统拿数据
        res += recv_data  # 拼接到res
        recv_size += len(recv_data)  # 计数器加上拿到的数据的长度
    print(res.decode("gbk"))

client.close()
终极解决方法-客户端

UDP协议的套接字

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

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

 udp套接字简单示例

from socket import *
ss = socket()                                #创建一个服务器套接字
ss.bind()                                      #绑定服务器套接字
inf_loop:                                      #服务器无限循环
    cs = ss.recvfrom()/ss.sendto()   #对话(接收与发送)
ss.close()                                      #关闭服务器套接字
udp服务端
cs = socket()                         # 创建客户套接字
comm_loop:                          # 通讯循环
    cs.sendto()/cs.recvfrom()   # 对话(发送/接收)
cs.close()                              # 关闭客户套接字        
udp客户端

一个返回输入内容大写的c/s架构

from socket import *

server=socket(AF_INET,SOCK_DGRAM)
server.bind(('127.0.0.1',8083))

while True:
    data,client_addr=server.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组
    print('客户端的数据: ',data)
    server.sendto(data.upper(),client_addr) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组
服务端
from socket import *

client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input('>>: ').strip()

    client.sendto(msg.encode('utf-8'),('127.0.0.1',8083)) #sendto 第一个参数是要发送内容,第二个是目标ip端口元组
    data,server_addr=client.recvfrom(1024) # recvfrom 收到的内容第一个元素是对面发送的内容,第二个是对面的ip端口元组
    print(data.decode('utf-8'))
    print(server_addr)
客户端

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

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

 

 

 

posted @ 2017-11-29 15:38  瓜田月夜  阅读(127)  评论(0)    收藏  举报