day24-网络知识扫盲,socket的基本使用

温故而知新

昨天我们讲了. 网络编程.

网络编程里面有个socket模块.这个模块的可以让我们实现,C/S架构和B/S架构.

客户端						服务端
client---------网络---------->server
浏览器端					服务器端
boerdlow---------网络---------->server

我们根据这个架构图可以了解,要想实现不同计算机之间相互玩耍就必须要经过网络

所以我们昨天学了osi七层协议/tcp/ip五层协议

应用层   http,https/tfp协议...
会话层
表示层
输出层   ----> tcp/udp协议  端口号 
网络层	  ---> ip协议 单位包  数据分为:源ip地址,目标ip地址...数据
数据链路层 --> 将电信号分组 单位帧 以太网协议 数据分为:帧头(源mac,目标mac,数据类型)和数据部分 
物理层  ---> 电信号 高低点频 0101

主要介绍了 tcp的三次握手和四次挥手

扫盲

那我们今天就对昨天的知识进行一次扫盲

对ip进行扫盲

ip 协议,单单只有一个ip是没有用途的,与其搭配的还有子网掩码和网关

现在的主流ip有ipv4和ipv6

ipv4是一个由32位的二进制构成的,而ipv6是一个由48位二进制构成的.

因为ipv4是一个32位的二进制,随着现在的计算机数量越来越多,慢慢发现ipv4这个2 的 32 次方个ip已经告急了.所以现在慢慢的退出了ipv6.但现在的ip主流市场还是ipv4

我们就对ipv4来进行一个扫盲

官方一点的知识来了

ip协议:

  • 规定网络地址的协议叫做ip协议,它定义的地址称之为ip地址,它规定网络地址由32位二进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个IP地址通常写成四段十进制数,例如:192.168.3.11

ip地址分为俩大类

  • 一类为网络位:标识子网
  • 一类位主机位:标识主机

注意:单纯的ip地址只是标识了ip地址的种类,从网络部分和主机部分都无法辨别一个ip的子网

子网掩码

所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。

知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络 方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。 \

arp协议

还有就是arp地址解析协议

地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址物理地址/2129)的一个TCP/IP协议。主机主机/455151)发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询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

DNS 域名解析协议

域名系统(英文:Domain Name System,缩写):DNS)是互联网的一项服务。它作为将域名)和IP地址相互映射)的一个分布式数据库,能够使人更方便地访问互联网.DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。

所以它的功能就是将ip和域名之间进行转换,使得用户在访问一个地址时,更加的方便.

URL

什么是URL:

URL是一个由 协议名 + 域名 (ip + 80) + 虚拟路径 (要访问的数据的路径)

例如:https://www.cnblogs.com/jkeykey/p/14281653.html

其中协议名为:https

域名为 www.cnblogs.com ip 121.40.43.188 + 80

虚拟路径为: /jkeykey/p/14281653.html

那输入一个url会发生什么?

先根据其中的DNS域名解析确定到是哪个服务器
再通过TCP和服务器之间建立连接
并且发送https协议请求
服务器确认连接和协议请求,并根据虚拟路径去数据库(存放数据的地方)拿到数据的内容
再将拿到的内容返回.并断开连接
客户端拿到返回的数据即(HTML)
然后将这个html文件通过浏览器渲染到网页

socket套接字

那其实学了这么多的计算机网络的基础,为的就是我们接下来的主角socket

socket是应用层与TCP/IP协议簇通信的中间的软件抽象层,它是一组接口,为的就是方便我们不用再去深究传输层已经传输层以下的四层传输协议.它已经给我们把这个封装好了.

所以我们只需要去遵循socket的规定去编程,写的程序自然就是遵循TCP/UDP标准的.

也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序

而程序的pid是同一台机器上不同进程或者线程的标识

套接字发展史及分类

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

套接字工作流程

​ 一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。

img

​ 图3

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

socket用法

重点来了.实际用法

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)

服务端套接字函数

s.bind((ip,port))	# 绑定(主机,端口号) 到套接字
s.listen(最大本连接池数)  # 开始TCP监听
conn,addr = s.accept()  # 被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数

c.connent()		# 主动初始化TCP服务器连接
c.connect_ex()  # connnet() 函数的拓展版,出错时,返回出错码,而不是抛出异常

公共用途的套接字函数

# 公共用途的套接字函数
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的服务端和客户端交流的小程序

服务端

# -*- coding: utf-8 -*-
# @Author  : JKey  
# Timer    : 2021/1/15 10:39

import socket
import subprocess

# 相当于买了一个手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
# 相当于给手机插上了手机卡
phone.bind(('192.168.12.61', 8081))  
# 相当于一次最大能接5个人的电话
phone.listen(5)

while True:
    # 相当于等待对方打电话的过程
    conn, client_addr = phone.accept()  # conn表示套接字对象  client_addr 表示客户端的ip和端口
    print(client_addr)

    while True:
        try:  # 当服务器还在连接中,而客户端强行断开连接时,windows会直接抛异常 
            data = conn.recv(1024)  # 接收的是一个二进制
            if len(data) == 0:  # 而Linux会在判断收到了一个空数据,会一直循环收
                break
            cmd = data.decode('utf-8')  # 需要转成字符串
            s = subprocess.Popen(cmd, shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE
                                 )  # 创建一个子进程,让用户输入什么就返回shell语句的结果
            res1 = s.stdout.read()
            res2 = s.stderr.read()
            conn.send(res1 + res2)
        except ConnectionResetError:  # 客户端非正常断开连接时widows抛出的异常类型
            break

    conn.close()  # 关闭客户端连接
phone.close()  # 关闭手机

客户端

# -*- coding: utf-8 -*-
# @Author  : JKey  
# Timer    : 2021/1/15 10:39
import socket

# 获取客户端套接字对象
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 客户端向对应得服务端发连接请求
client.connect(('192.168.12.61', 8081))

while True:
    i = input('>>:').strip()
    if len(i) == 0:continue
    client.send(i.encode('utf-8'))  # 客户端发送信息

    data = client.recv(1024)  # 客户端接收信息

    print(data.decode('gbk'))

client.close()


再来一个B/S的架构的小程序

# -*- coding: utf-8 -*-
# @Author  : JKey  
# Timer    : 2021/1/14 18:36

# 模拟浏览器 访问新浪微博

import socket
import ssl

# s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s = ssl.wrap_socket(socket.socket())
s.connect(('www.sina.com.cn', 443))

s.send(b'GET / HTTP/1.1\r\nHost:www.sina.com.cn\r\nConnection: close\r\n\r\n')


buffer = []
while True:

    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break

data = b''.join(buffer)

header, html = data.split(b'\r\n\r\n', 1)
print(header.decode("utf-8"))
with open('sina.html', 'wb') as f:
    f.write(html)

问题:

有的同学在重启服务端时可能会遇到

img

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的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))
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
posted on 2021-01-15 21:17  Jkeykey  阅读(149)  评论(0编辑  收藏  举报