第十四章 网络编程

1.网络基础                                                    

参考:http://www.cnblogs.com/Eva-J/articles/8066842.html

2.软件开发架构                                                

C/S架构:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。

B/S架构:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。(其实B/S架构也是一种C/S架构)

3.计算机网络                                                  

1.在网络中的两个程序通过IP和PORT找到对方

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。

2.socket概念

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

3.套接字(socket)介绍

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

 -- 套接字的分类

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

  套接字家族的名字:AF_UNIX

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

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

  套接字家族的名字:AF_INET

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

4.传输协议价绍

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

我知道说这些你们也不懂,直接上图。

4.套接字(socket)的使用                                      

4.1基于TCP协议的socket

特点:TCP链接服务端同一时间只能与一个客户端进行通信,服务端与客户端一旦建立连接,将一直占用socket链接

1- 简单版TCP通信

server端

import socket

# sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk = socket.socket()  # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP
sk.bind(('127.0.0.1', 9000))  # 把地址绑定到套接字
sk.listen()  # 监听链接
conn, addr = sk.accept()  # 接收客户端链接
print(addr) # ('127.0.0.1', 51625) 客户端的地址
conn.send('haha'.encode('utf-8'))  # 向客户端发送消息,消息是bytes类型
ret = conn.recv(1024).decode('utf-8')  # 接收客户端消息,需要解码 1024--最大接收1024个字节
print(ret)  # 打印客户端消息
conn.close()  # 关闭客户端套接字
sk.close()  # 关闭服务器套接字

client端

import socket

# sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk = socket.socket()  # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP
sk.connect(('127.0.0.1', 9000))  # 尝试连接服务器
ret = sk.recv(1024).decode('utf-8')  # 陷入阻塞,客户端接收消息,并解码 1024--最大接收1024个字节
print(ret)
sk.send('hahahaha'.encode('utf-8'))  # 客户端发送消息
sk.close()  #关闭客户端套接字

2- 异常处理 -- 端口被占用

# 异常描述
#     sk.bind(('127.0.0.1', 9000))  # 把地址绑定到套接字
# OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
# 解决方法:
# 加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR

# sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk = socket.socket()  # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) ####### 就是它,在bind前加
sk.bind(('127.0.0.1', 9000))  # 把地址绑定到套接字
sk.listen()  # 监听链接
conn, addr = sk.accept()  # 接收客户端链接
print(addr) # ('127.0.0.1', 51625) 客户端的地址
conn.send('haha'.encode('utf-8'))  # 向客户端发送消息,消息是bytes类型
ret = conn.recv(1024).decode('utf-8')  # 接收客户端消息,需要解码
print(ret)  # 打印客户端消息
conn.close()  # 关闭客户端套接字
sk.close()  # 关闭服务器套接字

3- 单间循环版TCP通信

server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, addr = sk.accept()
print('%s正在与你通信'%addr[0])
while True:
    msg = input('>>> ')
    conn.send(msg.encode('utf-8'))
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
conn.close()
sk.close()

client端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
while True:
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    msg = input('>>> ')
    sk.send(msg.encode('utf-8'))
sk.close()

4- 带退出的循环版TCP通信

server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, addr = sk.accept()
print('%s正在与你通信'%addr[0])
while True:
    msg = input('>>> ')
    conn.send(msg.encode('utf-8'))
    if msg == 'q':break
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    if ret == 'q':break
conn.close()
sk.close()

client端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
while True:
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == 'q':break
    msg = input('>>> ')
    sk.send(msg.encode('utf-8'))
    if msg == 'q':break
sk.close()

5- 一个服务端对应多个客户端,但一次只能与一个客户端通信,当这个客户端断开连接了,依次与后面的客户端进行通信

server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
while True:
    conn, addr = sk.accept()
    print('%s正在与你通信'%addr[0])
    while True:
        msg = input('>>> ')
        conn.send(msg.encode('utf-8'))
        if msg == 'q':break
        ret = conn.recv(1024).decode('utf-8')
        print(ret)
        if ret == 'q':break
    conn.close()
sk.close()

client端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
while True:
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == 'q':break
    msg = input('>>> ')
    sk.send(msg.encode('utf-8'))
    if msg == 'q':break
sk.close()

6- 简单版ssh,windows版

server端

import socket
import subprocess

ip_port = ('127.0.0.1', 9000)
BUFSIZE = 1024

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(ip_port)
sk.listen()

while True:
    conn,addr = sk.accept()
    print('客户端:', addr)

    while True:
        cmd = conn.recv(BUFSIZE)
        if len(cmd) == 0:break

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

        stderr = res.stderr.read()
        stdout = res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)

client端

import os
import socket

BUFSIZE=1024
ip_port = ('127.0.0.1', 9000)

sk = socket.socket()
sk.connect_ex(ip_port)

while True:
    cmd = input(os.getcwd() + '> ')
    if not cmd:continue
    sk.send(cmd.encode('utf-8'))
    res = sk.recv(BUFSIZE).decode('gbk')
    print(res)

演示

# server
客户端: ('127.0.0.1', 60670)

# client
E:python学习目录模拟ssh> dir
 驱动器 E 中的卷是 新加卷
 卷的序列号是 3C42-57C9

 E:python学习目录模拟ssh 的目录

2018/06/09  15:09    <DIR>          .
2018/06/09  15:09    <DIR>          ..
2018/06/09  15:09               288 client.py
2018/06/09  15:09             1,032 server.py
2018/06/09  14:51                 0 __init__.py
               3 个文件          1,320 字节
               2 个目录 374,906,806,272 可用字节

7- 使用struct解决粘包问题

server端

import socket
import struct
import subprocess

ip_port = ('127.0.0.1', 9000)
BUFSIZE = 1024

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(ip_port)
sk.listen()

while True:
    conn,addr = sk.accept()
    print('客户端:', addr)

    while True:
        cmd_len = conn.recv(4)
        cmd = conn.recv(struct.unpack('i', cmd_len)[0])
        res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
                               stdout=subprocess.PIPE,
                               stdin=subprocess.PIPE,
                               stderr=subprocess.PIPE)

        stderr = res.stderr.read()
        print('stderr----', stderr)
        conn.send(stderr)
        stdout = res.stdout.read()
        print('stdout----', stdout)
        conn.send(stdout)

client端

import os
import socket
import struct

BUFSIZE=1024
ip_port = ('127.0.0.1', 9000)

sk = socket.socket()
sk.connect_ex(ip_port)

while True:
    cmd = input(os.getcwd() + '> ')
    if not cmd: continue
    cmd_len = len(cmd)
    bytes_cmd = struct.pack('i', cmd_len)
    sk.send(bytes_cmd)
    sk.send(cmd.encode('utf-8'))
    res = sk.recv(BUFSIZE).decode('gbk')
    print(res)

演示

# server
客户端: ('127.0.0.1', 52647)
stderr---- b''
stdout---- b' xc7xfdxb6xafxc6xf7 E xd6xd0xb5xc4xbexedxcaxc7 xd0xc2xbcxd3xbexed
 xbexedxb5xc4xd0xf2xc1xd0xbaxc5xcaxc7 3C42-57C9

 E:\pythonxd1xa7xcfxb0xc4xbfxc2xbc\pythonxd1xa7xcfxb0xd6xaexc2xc3\xd1xa7xcfxb0xd6xaexc2xc3\day08 xb8xb4xcfxb0\xc4xa3xc4xe2ssh xb5xc4xc4xbfxc2xbc

2018/06/09  15:30    <DIR>          .
2018/06/09  15:30    <DIR>          ..
2018/06/09  15:26               288 c1.py
2018/06/09  15:23               395 client.py
2018/06/09  15:26               715 s1.py
2018/06/09  15:30               853 server.py
2018/06/09  14:51                 0 __init__.py
               5 xb8xf6xcexc4xbcxfe          2,251 xd7xd6xbdxda
               2 xb8xf6xc4xbfxc2xbc 374,906,802,176 xbfxc9xd3xc3xd7xd6xbdxda
'
stderr---- b"'sl' xb2xbbxcaxc7xc4xdaxb2xbfxbbxf2xcdxe2xb2xbfxc3xfcxc1xeexa3xacxd2xb2xb2xbbxcaxc7xbfxc9xd4xcbxd0xd0xb5xc4xb3xccxd0xf2
xbbxf2xc5xfaxb4xa6xc0xedxcexc4xbcxfexa1xa3
"
stdout---- b''

# client
E:python学习目录模拟ssh> dir
 驱动器 E 中的卷是 新加卷
 卷的序列号是 3C42-57C9

 E:python学习目录模拟ssh 的目录

2018/06/09  15:30    <DIR>          .
2018/06/09  15:30    <DIR>          ..
2018/06/09  15:26               288 c1.py
2018/06/09  15:23               395 client.py
2018/06/09  15:26               715 s1.py
2018/06/09  15:30               853 server.py
2018/06/09  14:51                 0 __init__.py
               5 个文件          2,251 字节
               2 个目录 374,906,802,176 可用字节

E:python学习目录模拟ssh> sl
'sl' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

4.2基于UDP协议的socket

特点:没有建立链接的过程,可以同时接受多个客户端的链接

1- 简单版UDP通信

server端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # ******-******必须添加协议类型
sk.bind(('127.0.0.1', 9000))  # 绑定服务器套接字
msg, addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))
sk.sendto(b'byebye', addr) # 需要添加客户端的地址
sk.close()

client端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # ******-******必须添加协议类型
sk.sendto(b'hello', ('127.0.0.1', 9000))
msg,addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))
sk.close()

2- 循环版支持多个客户端UDP通信

server端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型
sk.bind(('127.0.0.1', 9000))  # 绑定服务器套接字
while True:
    msg, addr = sk.recvfrom(1024)
    print(addr,':',msg.decode('utf-8'))
    inp = input('>>> ')
    sk.sendto(inp.encode('utf-8'), addr) # 需要添加客户端的地址
sk.close()

client端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型
inp = input('>>> ')
sk.sendto(inp.encode('utf-8'), ('127.0.0.1', 9000))
msg,addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))
sk.close()

演示

# server:
('127.0.0.1', 59120) : 你好
>>> 不好
('127.0.0.1', 53072) : 好不好
>>> 不太好
# client1:
>>> 你好
不好
# client2:
>>> 好不好
不太好

3- 并发循环版UDP通信

server端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型
sk.bind(('127.0.0.1', 9000))  # 绑定服务器套接字
while True:
    msg, addr = sk.recvfrom(1024)
    print(addr,':',msg.decode('utf-8'))
    inp = input('>>> ')
    sk.sendto(inp.encode('utf-8'), addr) # 需要添加客户端的地址
sk.close()

client端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型
while True:
    inp = input('>>> ')
    sk.sendto(inp.encode('utf-8'), ('127.0.0.1', 9000))
    msg,addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
sk.close()

演示

# server:
('127.0.0.1', 55844) : 哈哈哈
>>> 不哈
('127.0.0.1', 59756) : 2哈2哈
>>> 不2哈
# client1:
>>> 哈哈哈
不哈
>>>
# client2:
>>> 2哈2哈
不2哈
>>>

4- ntp时钟服务

server端

import socket
from time import strftime

ip_port = ('127.0.0.1', 9000)
bufsize = 1024

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(ip_port)

while True:
    msg, addr = sk.recvfrom(bufsize)
    msg = msg.decode('utf-8')
    if msg == 'ntpdate':
        print('%s 正在与你进行时间同步'%addr[0])
        time_fmt = '%Y-%m-%d %X'
        back_msg = strftime(time_fmt)
        sk.sendto(back_msg.encode('utf-8'), addr)
sk.close()

client端

import socket
import time

ip = input('请输入时钟服务器的IP:')
while True:
    port = input('请输入时间同步端口:')
    if port.isdigit():
        port = int(port)
        break
    else:
        print('输入的端口错误')
        continue

ip_port=(ip, port)
bufsize=1024
sk = socket.socket(type=socket.SOCK_DGRAM)

while True:
    msg='ntpdate'
    sk.sendto(msg.encode('utf-8'),ip_port)
    data=sk.recv(bufsize)
    print(data.decode('utf-8'))
    time.sleep(10)

sk.close()

演示

# server
127.0.0.1 正在与你进行时间同步
127.0.0.1 正在与你进行时间同步
127.0.0.1 正在与你进行时间同步

# client
请输入时钟服务器的IP:127.0.0.1
请输入时间同步端口:9000
2018-06-09 14:45:19
2018-06-09 14:45:29
2018-06-09 14:45:39

#5- 

#备注

4.3socket参数的详解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

  family  
地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type  
套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto  
协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno  
如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

5.粘包                                                             

说明:同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是粘包。

1- 发送方的缓存机制

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()

conn,addr = sk.accept()
data1 = conn.recv(15).decode('utf-8')
data2 = conn.recv(15).decode('utf-8')
print('--->1: ',data1)
print('--->2: ',data2)
conn.close()
sk.close()

client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
sk.send('hello'.encode('utf-8'))
sk.send('alex'.encode('utf-8'))
sk.close()

运行结果

# server
--->1:  helloalex
--->2:

粘包只会发生在tcp协议中,在连续send的过程中就容易发生粘包

2- 接收方的缓存机制

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()

conn,addr = sk.accept()
data1 = conn.recv(2).decode('utf-8')
data2 = conn.recv(15).decode('utf-8')
print('--->1: ',data1)
print('--->2: ',data2)
conn.close()
sk.close()

client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
sk.send('hello'.encode('utf-8'))
sk.send('alex'.encode('utf-8'))
sk.close()

运行结果

# server
--->1:  he
--->2:  lloalex

总结:

粘包现象只发生在tcp协议中:

1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

6.粘包的解决                                                  

原理:提前计算要发送数据的长度,相关模块struct

import struct
ret = struct.pack('i', 111111)
print(ret, len(ret))

# b'x07xb2x01x00' 4
# 说明:'i' 表示 int数据类型
# 返回一个4个字节的bytes字符串
# struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

res = struct.unpack('i', ret)
print(res)
# (111111,) # 只有一个元素的元组

粘包问题的解决

server端

import socket
import struct
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()

conn,addr = sk.accept()
inp = input('>>>> ').encode('utf-8')
inp_len = len(inp)
bytes_msg = struct.pack('i', inp_len)
conn.send(bytes_msg)
conn.send(inp)
conn.close()
sk.close()

client端

import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
msg_len = struct.unpack('i',sk.recv(4))[0]
print(sk.recv(msg_len).decode('utf-8'))
sk.close()

运行结果

# server
>>>> 哈哈哈哈哈
# client
哈哈哈哈哈

7.socketserver并发网络编程                                    

socket模块的局限性:使用TCP协议在同一时刻,server端只能与一个客户端通信

socket模块是底层模块,socketserver模块是基于socket模块添加功能封装的模块

参考:http://www.cnblogs.com/Eva-J/p/5081851.html -- 解读socketserver源码

server端

import time
import socketserver

class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        conn = self.request
        print(conn)
        print(self.client_address)

        conn.send(b'hello')
        time.sleep(5)
        conn.send(b'hello2')

myserver = socketserver.ThreadingTCPServer(('127.0.0.1',9000),Myserver)
myserver.serve_forever()

client端

import socket
import time

sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
print(sk.recv(1024))
print(sk.recv(1024))
time.sleep(20)
sk.close()

运行结果

# server
<socket.socket fd=500, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53135)>
('127.0.0.1', 53135)
<socket.socket fd=516, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53136)>
('127.0.0.1', 53136)
<socket.socket fd=544, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53137)>
('127.0.0.1', 53137)

# client -- 可以同时开启多个客户端
b'hello'
b'hello2'
# client
b'hello'
b'hello2'
# client
b'hello'
b'hello2'
原文地址:https://www.cnblogs.com/gongniue/p/9154928.html