socket

客户端/服务器架构

C/S架构与socket的关系:

我们学习socket就是为了完成C/S架构的开发

osi七层:

应用层:

功能:提供用户接口

软件:QQ/微信、浏览器等各种能上网的应用程序

协议:HTTP、HTTPS、OICQ、Telnet、SSH等

    提供用户接口,特指能够发起网络通信的应用程序。实际上,会话层,表示层、应用层也可以统称为应用层

 

问题:在现代软件开发中,如果我们编码还要根据不同编码方式进行代码实现,那么就不断进行重复劳动了。我们可以将表示层再进行包装吗?

 

解决方案:增加一层"应用层"。

 

表示层:

功能:担当数据的显示

    使用何种编码方式。比如要传输的数据使用ASCII编码,视频还是二进制文件,是否要加密和压缩。发送端和接收端程序必须使用相同的编码方式,才能正确显示,否则就产生乱码。

问题:在现代软件开发中,如果我们编码还要根据不同编码方式进行代码实现,那么就不断进行重复劳动了。我们可以将表示层再进行包装吗?

解决方案:增加一层"应用层"。

会话层

功能:担当会话管理。

在两台电脑间,两个不同的应用程序间的:建立会话,区别于其他应用程序间的会话(如QQ的信息不会发送到浏览器中,使用端口号进行区分),保持会话,删除会话的过程。

问题:我们两台电脑间的通信,不仅仅是文字的交互。而是有多种数据格式的。那么会话层提供不了这个功能。

解决方案:增加一层"表示层"

 

传输层

功能:担当了可靠的端对端链接。

协议:TCP、UDP

 

    提供了可靠或不可靠传输,能够纠正或失败重传,传输层通过端口号区分上层服务,并通过滑动窗口技术实现可靠传输、流量控制、拥塞控制等。传输层负责的是计算机之间的链接。

问题:尽管传输层提供了可靠的链接,那么当有一个发送方对多个接收方时,我们如何确定数据传输给哪一个接收方呢?又如何与其建立链接、保持链接、删除链接呢?

解决方案:增加一层"会话层"

网络层

功能:提供了三层寻址,三层数据转发功能

设备:路由器

协议:IP协议

路由器主要是根据IP地址来进行不同PC间的通信的。虽然路由器工作再网络层,但它实际上是兼有数据链路层、物理层的。所以在同一路由器下,IP地址、MAC地址不能相同,否则会发生冲突。当然路由器也存在一个IP地址,用于跟别的路由器进行通信,这样就可以屏蔽不同局域网协议间不能通信的问题了。

问题:仅仅通过路由器简单的发送数据可不行,如果因为网络的问题,导致数据丢失,数据传输不可控。这样就使得网络通信不可靠

解决方案:在网络层基础上,增加"传输层"

 

数据链路层

 

功能:提供了二层寻址、二层数据转发功能。

 

设备:网桥、交换机

 

协议:PPP、Ethernet、ARP、RARP..

 

数据链路层在不同的厂商有不同的实现,主要应用于没有路由器的情况下。多台电脑相互通信的情况,这种网络称为局域网。同一局域网中,MAC地址不能相同。

 

问题:不同协议间肯定是不能进行通信的。那么我们该如何使两个局域网之间进行通信呢?

解决方案:增加一层"网络层"

 

物理层

功能:提供物理规范,如线缆标准,接口标准

 

                               互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层                                    

具体通信原理可看:

http://www.cnblogs.com/linhaifeng/articles/5937962.html

使用 socket一定要先学习互联网协议

C/S架构的软件(软件属于应用层)是基于网络进行通信的,网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。

 

 

 

socket层

socket是什么

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

套接字工作流程

简单的socket实例:

服务端

__author__ = 'zyp'
#-*- coding:utf-8 -*-
import socket
import os
server = socket.socket()
server.bind(('localhost',6969))
server.listen(5)
while True:
    print("我在等电话")
    conn,addr=server.accept()
    print("电话来了")
    while True:
        data = conn.recv(1024)
        print('recv:',data.decode())
        if not data :
            print("输入为空!")
            break
        res = os.popen(data.decode()).read()

        conn.send(res.encode())

server.close()
服务端

客户端

客户端

 socket()模块函数用法

 1 import socket
 2 socket.socket(socket_family,socket_type,protocal=0)
 3 socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
 4 
 5 获取tcp/ip套接字
 6 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 7 
 8 获取udp/ip套接字
 9 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 10 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',
我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
11 例如tcpSock = socket(AF_INET, SOCK_STREAM)
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
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() 关闭套接字

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

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

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

tcp服务端

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()            # 关闭客户套接字

如果在重启服务端时可能会遇到:地址被占用

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址

解决方法:

#加入一条socket配置,重用ip和端口

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

基于UDP的套接字

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

udp服务端

1 ss = socket()   #创建一个服务器的套接字
2 ss.bind()       #绑定服务器套接字
3 inf_loop:       #服务器无限循环
4     cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
5 ss.close()                         # 关闭服务器套接字

udp客户端

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

udp实例

# !usr/bin/env python
# -*-coding:utf-8 -*-
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(("localhost",6666))
while True:
    conn,addr = server.recvfrom(1024)
    print(conn.decode(),addr)
    server.sendto(conn.upper(),addr)
upd_server
# !usr/bin/env python
# -*-coding:utf-8 -*-
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
    msg = input(">>:").strip()
    if not msg:continue
    client.sendto(msg.encode("utf-8"),("localhost",6666))
    bak_msg,addr = client.recvfrom(1024)
    print(bak_msg.decode("utf-8"),addr)
udp_client

粘包现象

基于tcp的socket,在运行时会发生粘包

基于udp的socket,在运行时永远不会发生粘包

注意注意注意:

res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)

的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码

且只能从管道里读一次结果

注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果

 socket收发消息的原理

基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

两种情况下会发生粘包。

1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

 

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

send(字节流)和recv(1024)及sendall

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

 

解决粘包的方法

1.在连续发或者收中间再增加发和收。不建议使用

2.只收取相应文件大小

3.高级方法 struct模块 

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据:http://www.cnblogs.com/linhaifeng/articles/6129246.html#_label12

 

需要补充实例

sockerserver

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer

基于tcp的socketserver我们自己定义的类中的

  1.   self.server即套接字对象
  2.   self.request即一个链接
  3.   self.client_address即客户端地址

基于udp的socketserver我们自己定义的类中的

  1.   self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  2.   self.client_address即客户端地址

 

1.建立一个请求处理类,并继承这个类要继承BaseRequestHandler

2.重写父类的handle()   #这里处理与客户端的交互

3.实例化TCPServer,并且传递server ip 和 你创建的请求处理类 给这个TCPServer

4.   server.handle_request() #只处理一个请求

      server.serve_forever() #处理多个一个请求,永远执行

server:

import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote".format(self.client_address[0]))
                print(self.data)
                self.request.send(self.data.upper())
            except ConnectionResetError as e:
                print("error",e)
                break
HOST,PORT = "localhost",9999
selver = socketserver.TCPServer((HOST,PORT),MyTCPHandler)
selver.serve_forever()

client:

import socket
client = socket.socket()
client.connect(("localhost",9999))
while True:
    data = input(">>:").strip()
    if len(data) == 0:
        continue
    client.send(data.encode())
    res = client.recv(1024).decode()
    print("res",res)

client.close()

多并发

selver = socketserver.THreadingTCPServer((HOST,PORT),MyTCPHandler)

posted @ 2018-04-10 20:40  Aline2  阅读(157)  评论(0编辑  收藏  举报