网络编程

一.C/S架构

C指的是client(客户端软件),S指的是Server(服务端软)。
 
网络传送数据流程
1、客户端软件产生数据,存放于客户端软件的内存中,然后调用接口将自己内存中的数据发送/拷贝给操作系统内存
2、客户端操作系统收到数据后,按照客户端软件指定的规则(即协议)、调用网卡发送数据
3、网络传输数据
4、服务端软件调用系统接口,想要将数据从操作系统内存拷贝到自己的内存中
5、服务端操作系统收到4的指令后,使用与客户端相同的规则(即协议)从网卡接收到数据,然后拷贝给服务端软件
 
 

二.tcp/ip 5层

tcp/ip就是Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础
 
人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层,可以说互联网协议就是计算机世界的英语,简单来说就是规定一个统一的通信标准,让大家都用这个通信标准来进行通信。
 

三.TCP/IP各层详解

因为自上而下每层都依赖下一层,所以我们从最下一层开始说。

1.物理层

主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0

2.数据链路层

数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
数据链路层的功能:定义了电信号的分组方式
以太网协议:
早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet
ethernet规定
  • 一组电信号构成一个数据包,叫做‘帧’
  • 每一数据帧分成:报头head和数据data两部分 

head包含:(固定18个字节)
  • 发送者/源地址,6个字节
  • 接收者/目标地址,6个字节
  • 数据类型,6个字节
data包含:(最短46字节,最长1500字节)
  • 数据包的具体内容
head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
mac地址:
head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
 
广播:
有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)
ethernet采用最原始的方式,广播的方式进行通信,即计算机通信基本靠吼
 

3.网络层

网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由
一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,
这就不仅仅是效率低的问题了,这会是一种灾难
上图结论:必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,
就采用路由的方式(向不同广播域/子网分发数据包),mac地址是无法区分的,它只跟厂商有关
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址

IP协议:

  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1 
 

子网掩码

所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。
 
知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
 
比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,
172.16.10.1:10101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
 
172.16.10.2:10101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
结果都是172.16.10.0,因此它们在同一个子网络。
总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
 

ip数据包

ip数据包也分为head和data部分,无须为ip包定义单独的栏位,直接放入以太网包的data部分
 
head:长度为20到60字节
data:最长为65,515字节。
而以太网数据包的”数据”部分,最长只有1500字节。因此,如果IP数据包超过了1500字节,它就需要分割成几个以太网数据包,分开发送了。
 

ARP协议

arp协议由来:计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送,在谈及以太网协议时候,我门了解到
通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的,如何获取目标主机的mac,就需要通过arp协议
arp协议功能:广播的方式发送数据包,获取目标主机的mac地址
 
协议工作方式:每台主机ip都是已知的
例如:主机172.16.10.10/24访问172.16.10.11/24
一:首先通过ip地址和子网掩码区分出自己所处的子网
 
场景
数据包地址
同一子网
目标主机mac,目标主机ip
不同子网
网关mac,目标主机ip 

 

二:分析172.16.10.10/24与172.16.10.11/24处于同一网络(如果不是同一网络,那么下表中目标ip为172.16.10.1,通过arp获取的是网关的mac

 
 
源mac
目标mac
源ip
目标ip
数据部分
发送端主机
发送端mac
FF:FF:FF:FF:FF:FF
172.16.10.10/24
172.16.10.11/24
数据 

 

三:这个包会以广播的方式在发送端所处的自网内传输,所有主机接收后拆开包,发现目标ip为自己的,就响应,返回自己的mac

 

传输层

传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序,
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
传输层有两种协议,TCP和UDP,见下图
 

tcp协议

可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
 
 

udp协议

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

 
TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!
 

四.socket

1.什么是socket

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

 
socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
你想给另一台计算机发消息,你知道他的IP地址,他的机器上同时运行着qq、迅雷、word、浏览器等程序,你想给他的qq发消息,那想一下,你现在只能通过ip找到他的机器,但如果让这台机器知道把消息发给qq程序呢?答案就是通过port,一个机器上可以有0-65535个端口,你的程序想从网络上收发数据,就必须绑定一个端口,这样,远程发到这个端口上的数据,就全会转给这个程序啦
 
 
 
 

2.Socket通信套路

当通过socket建立起2台机器的连接后,本质上socket只干2件事,一是收数据,一是发数据,没数据时就等着。
 
socket 建立连接的过程跟我们现实中打电话比较像,打电话必须是打电话方和接电话方共同完成的事情,我们分别看看他们是怎么建立起通话的
 
接电话方(socket服务器端):
1.首先你得有个电话\(生成socket对象\)
 
2.你的电话要有号码\(绑定本机ip+port\)
 
3.你的电话必须连上电话线\(连网\)
 
4.开始在家等电话\(开始监听电话listen\)
 
5.电话铃响了,接起电话,听到对方的声音\(接受新连接\)
 
打电话方(socket客户端):
1.首先你得有个电话\(生成socket对象\)
 
2.输入你想拨打的电话\(connect 远程主机ip+port\)
 
3.等待对方接听
 
4.say “hi 约么,我有七天酒店的打折卡噢~”\(send\(\) 发消息。。。\)
 
5.等待回应——》响应回应——》等待回应。。。。
 
 

3.Socket套接字方法

family(socket家族)
  • socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成
  • socket.AF_INET:(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
socket type类型
  • socket.SOCK_STREAM #for tcp
  • socket.SOCK_DGRAM #for udp
  • socket.SOCK_RAW #原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
  • socket.SOCK_RDM #是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
  • socket.SOCK_SEQPACKET #废弃了
(Only SOCK_STREAM and SOCK_DGRAM appear to be generally useful.)
proto=0 请忽略,特殊用途
+
fileno=None 请忽略,特殊用途
 

4. 服务端套接字函数

  • s.bind() 绑定(主机,端口号)到套接字
  • s.listen() 开始TCP监听
  • s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来 

5. 客户端套接字函数

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

6. 公共用途的套接字函数

  • s.recv() 接收数据
  • s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
  • s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
  • s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
  • s.getpeername() 连接到当前套接字的远端的地址
  • s.close() 关闭套接字
  • socket.setblocking(flag) #True or False,设置socket为非阻塞模式,以后讲io异步时会用
  • socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo('luffycity.com',80)
  • socket.getfqdn() 拿到本机的主机名
  • socket.gethostbyname() 通过域名解析ip地址 

7. 简单的套接字循环:

服务端

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

phone.bind(('127.0.0.1', 8080))

phone.listen(5)

print('starting......')

while True:  # 链接循环
    conn, client_addr = phone.accept()
    print(client_addr)

    while True:  # 通信循环

        data = conn.recv(1024)
        if not data: break  # 在unix类的操作系统里,如果客户端断掉,会进入死循环。
        print('客户端的数据', data.decode('utf-8'))
        conn.send(data.upper())

    conn.close()

phone.close()

 

客户端

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.connect(('127.0.0.1', 8080))

while True:
    msg = input('>>>').strip()  # msg = ''
    if not msg: continue
    phone.send(msg.encode('utf-8'))  # phone.send(b'')
    data = phone.recv(1024)
    print(data.decode('utf-8'))

phone.close()

 

8.解决粘包

server

import socket
import subprocess
import struct
import json

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

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

server.listen(5)

print('starting......')

while True:
    conn, client_addr = server.accept()
    print(client_addr)

    while True:
        # 1、收命令
        cmd = conn.recv(8096)
        if not cmd: break

        # 2、执行命令,拿到结果
        obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        stdout = obj.stdout.read()
        stderr = obj.stderr.read()

        # 3、把命令的结果返回给客户端

        # 第一步:制作固定长度的报头

        header_dic = {
            'filename': 'a.txt',
            'md5': 'xxxxx',
            'total_size': len(stdout) + len(stderr)
        }
        header_json = json.dumps(header_dic)

        header_bytes = header_json.encode('utf-8')

        # 第二步:先发送报头的长度
        conn.send(struct.pack('i', len(header_bytes)))

        # 第三步:再发报头
        conn.send(header_bytes)

        # 第四步:再发送真实数据
        conn.send(stdout)
        conn.send(stderr)

    conn.close()

server.close()

 

client

import socket
import struct
import json

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.connect(('127.0.0.1', 9903))

while True:
    # 1、发命令
    cmd = input('>>>').strip()
    if not cmd: continue
    server.send(cmd.encode('utf-8'))

    # 拿命令的结果,并打印

    # 第一步:先收报头的长度
    obj = server.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 第二步:再收报头

    header_bytes = server.recv(header_size)

    # 第三步:从报头解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dict = json.loads(header_json)
    print(header_dict)
    total_size = header_dict['total_size']

    # 第三步:接收真实的数据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = server.recv(1024)
        recv_data += res
        recv_size += len(res)
    print(recv_data.decode('utf-8'))

server.close()

 

9. 基于udp的套接字

server

from socket import *

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

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

    server.sendto(data.upper(),client_addr)

server.close()

 

client

from socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>>:').strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))

    data, server_addr = client.recvfrom(1024)
    print(data, server_addr)

server.close()

 

 

 

 

 

posted @ 2018-12-25 13:30  梁少华  阅读(305)  评论(0编辑  收藏  举报