python12 网络编程

面向对象

一、面向对象是什么?

问题: 洗衣机里面放有脏衣服,怎么洗干净?

--------------------------------------- 面向过程的解决方法 ----------------------------------

1、把衣服放进洗衣机

2、放洗衣液

3、放水

4、洗衣服

5、倒掉脏水

6、再放水

7、再清洗

8、甩干衣服

9、晾晒

以上就是将解决这个问题的过程拆成一个个方法,通过一个个函数的执行来解决问题。

--------------------------------------- 面向对象的解决方法 ----------------------------------

1、我先弄出两个对象:"洗衣机"对象和"人"对象

2、针对对象“人”加入属性和方法:“把衣服放进洗衣机”、“放洗衣液”

3、针对对象"洗衣机"加入一些属性和方法:"放水"方法,"洗衣服"方法,"倒掉脏水"方法,"再放水"方法,"再清洗"方法,"甩干衣服"方法

4、然后执行

人.把衣服放进洗衣机

人.放洗衣液

洗衣机.放水

洗衣机.洗衣服

洗衣机.倒掉脏水

洗衣机.再放水

洗衣机.再清洗

人.甩干衣服

解决同一个问题 ,面向对象编程就是先抽象出所有相关对象,然后用对象执行方法的方式解决问题。

面向对象: 将问题里面涉及到的角色或者对象抽离出来

面向对象 (Object Oriented,OOP) 的思想对软件开发相当重要,它的概念和应用甚至已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD 技术、人工智能等领域。面向对象是一种 对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。

面向过程 (Procedure Oriented) 是一种 以过程为中心 的编程思想。

无论是在软件开发还是在实际工作中,深入地理解软件开发的思想都非常有必要。

二、类和实例

对象同时具有属性与方法两项特性

对象的属性与方法通常被封装到一起,用属性与方法来共同体现事物的特性,二者相辅相成

1528960497778

说一说教师里面的对象,描述他们的属性与方法

1528960673192

1528960737525

 

2.1 对象在代码中的体现

在分析现实生活中的事物时发现,这些事物都有其具体的特点和功能,这些特点和功能就组成了这个特殊的事物

我们来描述一辆小汽车:

分析:

事物的特点(属性):

颜色

品牌

事物的功能:

高速运行

发现:事物其实就是由特点(属性)和行为(功能)组成的。

可以简单的理解:属性就是数据,其实就是变量;行为就是功能,就是方法。
   
小汽车类
   颜色
   品牌
   def 高速运行():
       pass
   
在python语言中定义的类语法:
   class 类名: # 声明一个类 注意,如果由单词构成类名,建议大驼峰命名。
       '''
    解释文档
      '''
        类变量
        def 实例方法(self,*args,**kwargs):
            pass
           

2.2 实例变量(实例属性)

面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Person类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

案例演示:

class Person(object):
   pass

p1=Person()
p2=Person()

print(id(p1))
print(id(p2))

每一个人的实例对象都应该有自己的属性,比如姓名和年龄,实例变量的赋值如下:

# 实例变量的增删改查
p1.name="alex"
p1.age=18

p2.name="yuan"
p2.age=22

print(p1.gender)

del p2.age

这种方式需要先实例化再赋值实例变量,如何能在初始化对象的时候就将实例变量赋值好呢?这就要利用到类的魔法方法中最重要的构造方法( init)了:

# 定义Person类
class Person(object):
   def __init__(self,name,age):
       self.name=name
       self.age=age
       print(id(self))


# 实例化Person类的实例对象: 类实例对象=类名(实例化参数)
alex=Person("alex",18)
yuan=Person("yuan",22)

print(id(alex))

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

2.2 实例方法

实例方法或者叫对象方法,指的是我们在类中定义的普通方法。只有实例化对象之后才可以使用的方法,该方法的第一个形参接收的一定是对象本身。

class Person(object):

   def __init__(self,name,age):
       self.name=name
       self.age=age

   def print_info(self):
       print("姓名:%s,年龄:%s"%(self.name,self.age))


yuan = Person("yuan",18)
yuan.print_info()

2.3 一切皆对象

在python语言中,一切皆对象!

我们之前学习过的字符串,列表,字典等等数据都是一个个的类,我们用的所有数据都是一个个具体的实例对象。

区别就是,那些类是在解释器级别注册好的,而现在我们学习的是自定义类,但语法使用都是相同的。所以,我们自定义的类实例对象也可以和其他数据对象一样可以进行传参、赋值等操作。

class Weapon:

   def __init__(self,name,av,color):
       self.name=name
       self.av=av
       self.color=color
       

jiguangqiang=Weapon("激光枪",100,"red")

class Hero:

   def __init__(self,name,sex,hp,ce,weapon,level=2,exp=2000,money=10000): # 类必不可少的方法,用于实例化
       self.name=name  # 英雄的名字
       self.sex=sex # 英雄的性别
       self.hp=hp  # 英雄生命值
       self.level=level # 英雄的等级
       self.exp=exp # 英雄的经验值
       self.money=money # 英雄的金币
       self.weapon=weapon # 英雄的武器


alex=Hero("yuan","male",100,80,jiguangqiang)
print(alex.weapon.color)

def show_hero_weapon(obj):
   print(alex.weapon.color)

show_hero_weapon(alex)

2.4 类对象和类属性


class Student:

   # 类属性
   number = 68

   def __init__(self,name):
       self.name=name

yuan = Student("yuan")

# 实例对象和类对象可以获取类属性,但是只有类对象才能修改类属性
print(yuan.number)
print(Student.number)

yuan.number = 88

print(yuan.number)
print(Student.number)

2.5 静态方法和类方法

静态方法

定义:使用装饰器@staticmethod。参数随意,没有“self”和“cls”参数,但是方法体中不能使用类或实例的任何属性和方法;

调用:类对象或实例对象都可以调用。

class Cal():

   @staticmethod
   def add(x,y):
       return x+y

   @staticmethod
   def mul(x,y):
       return x*y

cal=Cal()
print(cal.add(1, 4))
or
print(Cal.add(3,4))

类方法

定义:使用装饰器@classmethod。第一个参数必须是当前类对象,该参数名一般约定为“cls”,通过它来传递类的属性和方法(不能传实例的属性和方法);

调用:类对象或实例对象都可以调用。

class Student:

   # 类属性
   number = 68

   @classmethod
   def add_number(cls):
       cls.number+=1
       print(cls.number)

Student.add_number()

问题:

  • 类对象.实例方法会怎么样?

  • 类方法的意义是什么,在实例方法中使用类对象变量不可以吗?

三、面向对象三大特性

3.1面向对象特性之继承(重点掌握)

面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。通过继承创建的新类称为子类派生类,被继承的类称为基类父类超类

class 派生类名(基类名)
  ...
3.1.1 继承的基本使用

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有得特性。例如猫有抓老鼠、爬树等其他动物没有的特性。同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如我们可以说猫是动物,但不能说动物是猫就是这个道理,其实对于这个我们将其称之为“向上转型”。

诚然,继承定义了类如何相互关联,共享特性。对于若干个相同或者相识的类,我们可以抽象出他们共有的行为或者属相并将其定义成一个父类或者超类,然后用这些类继承该父类,他们不仅可以拥有父类的属性、方法还可以定义自己独有的属性或者方法。

同时在使用继承时需要记住三句话:

1、子类拥有父类非私有化的属性和方法。

2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

3、子类可以用自己的方式实现父类的方法。(以后介绍)。


# 无继承方式

class Dog:

   def eat(self):
       print("eating...")

   def sleep(self):
       print("sleep...")

   def swimming(self):
       print("swimming...")

class Cat:

   def eat(self):
       print("eating...")

   def sleep(self):
       print("sleep...")

   def climb_tree(self):
       print("climb_tree...")


# 继承方式

class Animal:

   def eat(self):
       print("eating...")

   def sleep(self):
       print("sleep...")


class Dog(Animal):

   def swimming(self):
       print("toshetou...")

class Cat(Animal):

   def climb_tree(self):
       print("climb_tree...")


alex=Dog()
alex.run()
3.1.2 重写父类方法和调用父类方法
class Person(object):

   def __init__(self,name,age):
       self.name=name
       self.age=age


   def sleep(self):
       print("基类sleep...")



class Emp(Person):

  # def __init__(self,name,age,dep):
  #     self.name = name
  #     self.age = age
  #     self.dep = dep

  def __init__(self, name, age, dep):

      # Person.__init__(self,name,age)
      super().__init__(name,age)
      self.dep = dep



  def sleep(self):

       if "不在公司":
           # print("子类sleep...")
           # 调用父类方法
           # 方式1 :父类对象调用 父类对象.方法(self,其他参数)
           # Person.sleep(self)
           # 方式2: super关键字 super(子类对象,self).方法(参数)or super().方法(参数)
           super().sleep()



yuan = Emp("yuan",18,"教学部")

yuan.sleep()
print(yuan.dep)

测试题:

class Base:
   def __init__(self):
       self.func()
   def func(self):
       print('in base')

class Son(Base):
   def func(self):
       print('in son')

s = Son()

案例:电商项目的结算功能

电商项目,设计收银功能:

目前平台有三种商品:海尔冰箱每台3000元;西门子洗衣机每台5000元;芝华士沙发每件6000元。根据用户购买商品单价和数量计算总价。

-------------------------------------------    面向过程:


unit=int(input("请输入商品序号>>>"))
number=int(input("请输入商品数量>>>"))

goods={1:("海尔冰箱",3000),2:("西门子洗衣机",5000),3:("芝华士沙发",3000)}
total_price=goods[unit][1]*number
print(total_price)


-------------------------------------------    函数完善版:
def get_unit():


   unit=int(input("请输入商品序号>>>"))

   return unit


def get_number():

   number=int(input("请输入商品数量>>>"))

   return number

goods={1:("海尔冰箱",3000),2:("西门子洗衣机",5000),3:("芝华士沙发",6000)}

def get_total_price(unit,number):

   total_price=goods[unit][1]*number

   return total_price

unit=get_unit()
number=get_number()
total_price=get_total_price(unit,number)

print(total_price)

-------------------------------------------    面向对象版本
import datetime
import random


print('''

    1、海尔冰箱 单价3000元;
    2、西门子洗衣机 单价5000元;
    3、芝华士沙发 单价6000元

''')
goods={1:("海尔冰箱",3000),2:("西门子洗衣机",5000),3:("芝华士沙发",6000)}


# 结账类
class Bill(object):
   def __init__(self,name):
       self.name=name

   def get_unit(self):

       unit = int(input("请输入商品序号>>>"))

       return unit

   def get_number(self):

       number = int(input("请输入商品数量>>>"))

       return number

   def get_total_price(self):

       unit = self.get_unit()
       number = self.get_number()
       total_price = goods[unit][1] * number
       ret = self.discount(total_price)
       print("%s于%s总共花费%s元"%(self.name,datetime.datetime.today(),ret))
       return ret


   def discount(self,price):

       return price


# bill=Bill("Yuan老师")
# bill.get_total_price()



class NationalDayBill(Bill):

   def discount(self, price):
       if price>399:
           return price-200

       return super().discount(price)



# ndb=NationalDayBill("Yuan")
# ndb.get_total_price()


class Double11Bill(Bill):
   def discount(self,price):
       if price > 200:
           free=random.randint(0,1)
           if free == 0:
               return 0

       return super().discount(price)


# mab=Double11Bill("Yuan")
# mab.get_total_price()




# 问题:如果再加一个中秋节满五百折扣0.8,但是今年中秋节和国庆节是一天,所以两个优惠同时享受的话该怎样设计?


class MiddleAutumeBill(NationalDayBill):
   def discount(self,price):
       if price >500:
           temp_price=super().discount(price) # 国庆优惠后的价格
           return temp_price*0.8

       return super().discount(price)


mab=MiddleAutumeBill("Yuan")
mab.get_total_price()

3.1.3 多重继承

如果在继承元组中列了一个以上的类,那么它就被称作"多重继承" 。派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后,如下所示:

class SubClassName (ParentClass1[, ParentClass2, ...]):
  ...

多继承有什么意义呢?还拿上面的例子来说,蝙蝠和鹰都可以飞,飞的功能就重复定义了。

class Animal:

   def eat(self):
       print("eating...")

   def sleep(self):
       print("sleep...")


class Eagle(Animal):

   def fly(self):
       print("fly...")

class Bat(Animal):

   def fly(self):
       print("fly...")

有同学肯定想那就放到父类Animal中,可是那样的话其他不会飞的动物还怎么继承Animal呢?所以,这时候多重继承就发挥功能了:

class Fly:
   def fly(self):
       print("fly...")

class Eagle(Animal,Fly):
   pass

class Bat(Animal,Fly):
   pass
3.1.4 type 和isinstance方法
class Animal:

   def eat(self):
       print("eating...")

   def sleep(self):
       print("sleep...")


class Dog(Animal):
   def swim(self):
       print("swimming...")

alex = Dog()
mjj = Dog()

print(isinstance(alex,Dog))
print(isinstance(alex,Animal))
print(type(alex))
3.1.5、dir()方法和dict属性

dir(obj)可以获得对象的所有属性列表, 而obj.__dict__对象的自定义属性字典

注意事项:

  1. dir(obj)获取的属性列表中,方法也认为属性的一种。返回的是list

  2. obj.__dict__只能获取自己自定义的属性,系统内置属性无法获取。返回是dict


class Student:

   def __init__(self, name, score):
       self.name = name
       self.score = score

   def test(self):
       pass


yuan = Student("yuan", 100)
print("获取所有的属性列表")
print(dir(yuan))

print("获取自定义属性字段")
print(yuan.__dict__)

其中,类似__xx__的属性和方法都是有特殊用途的。如果调用len()函数视图获取一个对象的长度,其实在len()函数内部会自动去调用该对象的__len__()方法

3.2 面向对象特性之封装(理解)

封装概述

是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。

封装原则

  • 将不需要对外提供的内容都隐藏起来

  • 把属性隐藏,提供公共方法对其访问

封装好处

 

1、隐藏实现细节,提供公共的访问方式

2、提高了代码的复用性

3、提高安全性

 

3.2.1 私有属性

在class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的namescore属性:

class Student(object):

   def __init__(self, name, score):
       self.name = name
       self.score = score

alex=Student("alex",66)
yuan=Student("yuan",88)

alex.score=100
print(alex.score)

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:

class Student(object):

   def __init__(self, name, score):
       self.__name = name
       self.__score = score

alex=Student("alex",66)
yuan=Student("yuan",88)

print(alex.__score)

改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name实例变量.__score

这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。

但是如果外部代码要获取name和score怎么办?可以给Student类增加get_nameget_age这样的方法:

class Student(object):

   def __init__(self, name, score):
       self.__name = name
       self.__score = score

   def get_name(self):
       return self.__name

   def get_score(self):
       return self.__score

alex=Student("alex",66)
yuan=Student("yuan",88)

print(alex.get_name())
print(alex.get_score())

如果又要允许外部代码修改age怎么办?可以再给Student类增加set_age方法:

class Student(object):

   def __init__(self, name, score):
       self.__name = name
       self.__score = score

   def get_name(self):
       return self.__name

   def get_score(self):
       return self.__score

   def set_score(self,score):
       self.__score=score

alex=Student("alex",12)
print(alex.get_score())
alex.set_score(100)
print(alex.get_score())

你也许会问,原先那种直接通过alex.score = 100也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以设置值时做其他操作,比如记录操作日志,对参数做检查,避免传入无效的参数等等:

class Student(object):
  ...
   def set_score(self,score):
       if isinstance(score,int) and 0 <= score <= 100:
           self.__score = score
       else:
           raise ValueError('error!')

注意

2、这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:

_类名__属性,然后就可以访问了,如a._A__N

3、变形的过程只在类的内部生效,在定义后的赋值操作,不会变形

class Student(object):

   def __init__(self, name, score):
       self.name = name
       self.__score = score

   def get_score(self):
       return self.__score

yuan=Student("yuan",66)
print(yuan.__dict__)
yuan.__age=18
print(yuan.__dict__)

4、单下划线、双下划线、头尾双下划线说明:

  • __foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 __init__() 之类的。

  • _foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问。(约定成俗,不限语法)

  • __foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。


class Person(object):
   def __init__(self, name, score):
       self.__name = name
       self.__score = score

class Student(Person):

   def get_name(self):
       return self.__name

   def get_score(self):
       return self.__score

   def set_score(self,score):
       self.__score=score

yuan=Student("yuan",66)
print(yuan.__dict__)
print(yuan.get_name())
3.2.2 私有方法

在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的:

class Base:
   def foo(self):
       print("foo from Base")

   def test(self):
       self.foo()

class Son(Base):
   def foo(self):
       print("foo from Son")

s=Son()
s.test()


class Base:
   def __foo(self):
       print("foo from Base")

   def test(self):
       self.__foo()

class Son(Base):
   def __foo(self):
       print("foo from Son")

s=Son()
s.test()
3.2.3 property属性装饰器

使用接口函数获取修改数据 和 使用点方法(alex.name = ‘yuan’ 或者print(yuan.name))设置数据相比, 点方法使用更方便,我们有什么方法达到 既能使用点方法,同时又能让点方法直接调用到我们的接口了,答案就是property属性装饰器:

class Student(object):

   def __init__(self,name,score,sex):
       self.__name = name
       self.__score = score
       self.__sex = sex

   @property
   def name(self):
       return self.__name

   @name.setter
   def name(self,name):
       if len(name) > 1 :
           self.__name = name
       else:
           print("name的长度必须要大于1个长度")

   @property
   def score(self):
       return self.__score

   # @score.setter
   # def score(self, score):
   #     if score > 0 and score < 100:
   #         self.__score = score
   #     else:
   #         print("输入错误!")


yuan = Student('yuan',18,'male')

yuan.name = '苑浩'  # 调用了score(self, score)函数设置数据

print(yuan.name)   # 调用了score(self)函数获取数据

yuan.score = 199
print(yuan.score)

注意,使用 @property 装饰器时,接口名不必与属性名相同。

python提供了更加人性化的操作,可以通过限制方式完成只读、只写、读写、删除等各种操作


class Person:
   def __init__(self, name):
       self.__name = name

   def __get_name(self):
       return self.__name

   def __set_name(self, name):
       self.__name = name

   def __del_name(self):
       del self.__name
   # property()中定义了读取、赋值、删除的操作
   # name = property(__get_name, __set_name, __del_name)
   name = property(__get_name, __set_name)

yuan = Person("yuan")

print(yuan.name)   # 合法:调用__get_name
yuan.name = "苑浩"  # 合法:调用__set_name
print(yuan.name)

# property中没有添加__del_name函数,所以不能删除指定的属性
del p.name  # 错误:AttributeError: can't delete Attribute

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

3.3 面向对象特性之多态(了解)

3.3.1 多态的使用

多态是指一类事物有多种形态。比如动物有多种形态,人,狗,猫,等等。文件有多种形态:文本文件,可执行文件。总而言之,多态即某类的再分类,再分的每一类就是父类的多种形态的一种

from abc import ABCMeta,abstractmethod #(抽象方法)

class Payment(metaclass=ABCMeta):   # metaclass 元类 metaclass = ABCMeta表示Payment类是一个规范类
   def __init__(self,name,money):
       self.money=money
       self.name=name

   @abstractmethod      # @abstractmethod表示下面一行中的pay方法是一个必须在子类中实现的方法
   def pay(self,*args,**kwargs):
       pass

class AliPay(Payment):

   def pay(self):
       # 支付宝提供了一个网络上的联系渠道
       print('%s通过支付宝消费了%s元'%(self.name,self.money))

class WeChatPay(Payment):

   def pay(self):
       # 微信提供了一个网络上的联系渠道
       print('%s通过微信消费了%s元'%(self.name,self.money))


class Order(object):

   def account(self,pay_obj):
       pay_obj.pay()

pay_obj = WeChatPay("yuan",100)
pay_obj2 = AliPay("alex",200)

order = Order()
order.account(pay_obj)
order.account(pay_obj2)

从上述代码可见,在调用order对象的account()方法时,程序并不关心为该方法传入的参数对象pay_obj是谁,只要求此参数对象pay_obj包含pay()方法即可,而调用者传入的参数对象类型pay_obj是子类对象还是其他类对象,Python无所谓!多态性就是相同的消息(函数方法触发)使得不同的类做出不同的响应,这就是典型的类编程中多态性的应用实例。

"""
多态发生的前提:
  1、继承:多态一定是发生在子类和父类之间。
  2、重写:多态子类重写父类方法
"""
3.3.2 鸭子类型

在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。

例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

class Duck():
   def walk(self):
       print('I walk like a duck')
   def swim(self):
       print('i swim like a duck')

class Person():
   def walk(self):
    print('this one walk like a duck')
   def swim(self):
    print('this man swim like a duck')

可以很明显的看出,Person类拥有跟Duck类一样的方法,当有一个函数调用Duck类,并利用到了两个方法walk()swim()。我们传入Person类也一样可以运行,函数并不会检查对象的类型是不是Duck,只要他拥有walk()swim()方法,就可以正确的被调用。

// 静态语言
静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型。
例如:C++、Java、Delphi、C#等。

// 动态语言
动态语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。
例如PHP/ASP/Ruby/Python/Perl/ABAP/SQL/JavaScript/Unix Shell等等。

在我们上面的例子中order.account(pay_obj)中pay_obj不需要类型声明,像java在使用时要定义好类型

order.account(Payment pay_obj)),所以你传入别的类型对象一定报错,但是python因为时动态语言所以传入的对象只要拥有调用的方法即可视为Payment类型对象,即所谓的鸭子类型。

四、反射和魔法方法

4.1 反射

反射这个术语在很多语言中都存在,并且存在大量的运用,今天我们说说什么是反射,反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力,在python中一切皆对象(类,实例,模块等等都是对象),那么我们就可以通过反射的形式操作对象相关的属性。

Python中的反射主要有下面几个方法:

1.hasattr(object,name)

判断对象中有没有一个name字符串对应的方法或属性

2.getattr(object, name, default=None)

获取对象name字符串属性的值,如果不存在返回default的值

3.setattr(object, key, value)

设置对象的key属性为value值,等同于object.key = value

4.delattr(object, name)

删除对象的name字符串属性

应用1:

class Person:
   def __init__(self,name,age,gender):
       self.name = name
       self.age = age
       self.gender = gender

yuan=Person("yuan",22,"male")
print(yuan.name)
print(yuan.age)
print(yuan.gender)
while 1:
   # 由用户选择查看yuan的哪一个信息
   attr = input(">>>")
   if hasattr(yuan, attr):
       val = getattr(yuan, attr)
       print(val)
   else:
       val=input("yuan 没有你该属性信息!,请设置该属性值>>>")
       setattr(yuan,attr,val)

应用2:

class FTP(object):

   def __init__(self):
       self.run()

   def run(self):
      print('''
          提示:
              上传:   put 路径/文件名称
              下载:   get 路径/文件名称

      '''
      )
      while 1:
          input_str=input(">>>")
          action,params=input_str.split(" ")
          if hasattr(self,action):
              getattr(self,action)()
          else:
              print("不存在该方法")

   def put(self):
       print("上传...")
   def get(self):
       print("下载...")


ftp=FTP()

是不是比if-else或者字典映射要好一些呢?

4.2 魔法方法



# 1 初始化方法:__init__
class A(object):
   def __init__(self):
       print("初始化执行方法")
A()

# 2 构造方法:__new__
class B(object):

   def __new__(cls,*args,**kwargs):
       print("我是用来开辟一块空间的")
       obj=super().__new__(cls)
       return obj

   def __init__(self):
       print("self就是__new__开辟的空间地址")

B()

# 应用:比如单例模式

# 3 __str__

class C(object):
   def __init__(self,name,age):
       self.name=name
       self.age=age
   def __str__(self):  # 必须返回字符串类型
       return self.name

c1=C("c1",20)
c2=C("c2",23)
print(c1)
print(c2)

# 4 __call__
class D(object):

   def __call__(self, *args, **kwargs):
       print("call 被调用...")

print(callable(D))
d1=D()
print(callable(d1))
d1()

1.python 可调用对象:可以通过内置函数callable来判断,如:
print(callable(p))#True
2.如果类定义了__call__方法,那么它的实例可以变为可调用对象

# 5 析构方法 __del__

class F(object):
   def __del__(self):
       print("删除对象时被调用!")

f=F()
# del f # 思考:注释掉为什么也会调用__del__
# import time
# time.sleep(100)

# 应用

class Filehandler(object):
   file="a.text"
   def __init__(self):
       self.f=open(self.file)

   def __del__(self):
       self.f.close()

# 6 __getitem__


class G(object):
   def __init__(self):
       pass

   def __getitem__(self,item):
       print("__getitem__被调用")

   def __setitem__(self, key, value):
       print("__setitem__被调用")

   def __delitem__(self, key):
       print("__delitem__被调用")

g=G()
g["name"]="alex"
print(g["name"])
del g["name"]

# 7 __getattr__

class H(object):
   def __init__(self):
       pass

   def __getattr__(self, item):
       print("__getattr__被调用")

   def __setattr__(self, key, value):
       print("__setattr__被调用")

   def __delattr__(self, item):
       print("__delattr__被调用")

h=H()
h.name="alex"
print(h.name)
del h.name


# 8 __eq__

class I(object):
   def __init__(self,name,age):
       self.name=name
       self.age=age
   def __eq__(self, other):

       if self.name==other.name and self.age==other.age:
           return True
       else:
           return False

i1=I("alex",30)
i2=I("alex",30)
print(i1==i2)

问:
class Person(object):
   def __init__(self,name,age):
       self.name=name
       self.age=age

   def foo(self, other):
       pass
   def bar(self):
       pass
   def __eq__(self, other):
       print(1)
       return True
       # return self == value


alex=Person("alex",30)
yuan=Person("yuan",30)
print(id(yuan.foo) == id(alex.foo))
print(yuan.foo == alex.bar)
print(yuan.foo == alex.foo)
print(yuan.foo == yuan.foo)


# __len__

class G(object):
   def __len__(self):
       return 100

g=G()
print(len(g))


4.3 异常处理

当你的程序出现例外情况时就会发生异常(Exception)。例如,当你想要读取一个文件时,而那个文件却不存在,怎么办?又或者你在程序执报错了怎么办?这些通过使用异常来进行处理。类似地,如果你的程序中出现了一些无效的语句该怎么办?python 将会对此进行处理,举起(Raises)它的小手来告诉你哪里出现了一个错误(Error)。

当发生异常时,我们就需要对异常进行捕获,然后进行相应的处理。python的异常捕获常用try...except...结构,把可能发生错误的语句放在try模块里,用except来处理异常,每一个try,都必须至少对应一个except。此外,与python异常相关的关键字主要有:

关键字关键字说明
try/except 捕获异常并处理
pass 忽略异常
as 定义异常实例(except MyError as e)
else 如果try中的语句没有引发异常,则执行else中的语句
finally 无论是否出现异常,都执行的代码
raise 抛出/引发异常
4.3.1、捕获所有异常

包括键盘中断和程序退出请求,慎用。

try:
    <语句>
except:
    <语句>

案例:

try:
   import sys
   sys.exit()
except :
   print("999")
4.3.2、捕获指定异常
try:
    <语句>

except <异常名>:

     print('异常说明')

万能异常:

try:
    <语句>

except Exception:

     print('异常说明')

一个例子:


try:
   f = open("file-not-exists", "r")

except IOError as e:

   print("open exception: %s: %s" %(e.errno, e.strerror))
4.3.3、捕获多个异常

捕获多个异常有两种方式,第一种是一个except同时处理多个异常,不区分优先级:

try:
    <语句>

except (<异常名1>, <异常名2>, ...):

     print('异常说明')

第二种是区分优先级的:

try:
    <语句>

except <异常名1>:

     print('异常说明1')

except <异常名2>:

     print('异常说明2')

except <异常名3>:

     print('异常说明3')

该种异常处理语法的规则是:

  • 执行try下的语句,如果引发异常,则执行过程会跳到第一个except语句。

  • 如果第一个except中定义的异常与引发的异常匹配,则执行该except中的语句。

  • 如果引发的异常不匹配第一个except,则会搜索第二个except,允许编写的except数量没有限制。

  • 如果所有的except都不匹配,则异常会传递到下一个调用本代码的最高层try代码中。

4.3.4、异常中的else

如果判断完没有某些异常之后还想做其他事,就可以使用下面这样的else语句。


try:
    <语句>

except <异常名1>:

     print('异常说明1')

except <异常名2>:

     print('异常说明2')

else:

     <语句>  # try语句中没有异常则执行此段代码

img

使用 else 子句比把所有的语句都放在 try 子句里面要好,这样可以避免一些意想不到,而 except 又无法捕获的异常。

4.3.5、异常中的finally

try...finally...语句无论是否发生异常都将会执行最后的代码。

try:
    <语句>

finally:

     <语句>

img

4.3.6、raise主动触发异常

可以使用raise语句自己触发异常,raise语法格式如下:

raise [Exception [, args [, traceback]]]
4.3.7、常见异常类型
异常名称描述
   
BaseException 所有异常的基类
SystemExit 解释器请求退出
KeyboardInterrupt 用户中断执行(通常是输入^C)
Exception 常规错误的基类
StopIteration 迭代器没有更多的值
GeneratorExit 生成器(generator)发生异常来通知退出
StandardError 所有的内建标准异常的基类
ArithmeticError 所有数值计算错误的基类
FloatingPointError 浮点计算错误
OverflowError 数值运算超出最大限制
ZeroDivisionError 除(或取模)零 (所有数据类型)
AssertionError 断言语句失败
AttributeError 对象没有这个属性
EOFError 没有内建输入,到达EOF 标记
EnvironmentError 操作系统错误的基类
IOError 输入/输出操作失败
OSError 操作系统错误
WindowsError 系统调用失败
ImportError 导入模块/对象失败
LookupError 无效数据查询的基类
IndexError 序列中没有此索引(index)
KeyError 映射中没有这个键
MemoryError 内存溢出错误(对于Python 解释器不是致命的)
NameError 未声明/初始化对象 (没有属性)
UnboundLocalError 访问未初始化的本地变量
ReferenceError 弱引用(Weak reference)试图访问已经垃圾回收了的对象
RuntimeError 一般的运行时错误
NotImplementedError 尚未实现的方法
SyntaxError Python 语法错误
IndentationError 缩进错误
TabError Tab 和空格混用
SystemError 一般的解释器系统错误
TypeError 对类型无效的操作
ValueError 传入无效的参数
UnicodeError Unicode 相关的错误
UnicodeDecodeError Unicode 解码时的错误
UnicodeEncodeError Unicode 编码时错误
UnicodeTranslateError Unicode 转换时错误
Warning 警告的基类
DeprecationWarning 关于被弃用的特征的警告
FutureWarning 关于构造将来语义会有改变的警告
OverflowWarning 旧的关于自动提升为长整型(long)的警告
PendingDeprecationWarning 关于特性将会被废弃的警告
RuntimeWarning 可疑的运行时行为(runtime behavior)的警告
SyntaxWarning 可疑的语法的警告
UserWarning 用户代码生成的警告

 

网络编程

一、计算机网络三要素

网络编程:使用编程语言实现多台计算机的通信。

网络编程三大要素。

  (1)IP地址:网络中每一台计算机的唯一标识,通过IP地址找到指定的计算机。

  (2)端口:用于标识进程的逻辑地址,通过端口找到指定进程。

  (3)协议:定义通信规则,符合协议则可以通信,不符合不能通信。一般有TCP协议和UDP协议。 

1.1、TCP协议

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。

TCP数据报结构

我们先来看一下TCP数据报的结构:

img

带阴影的几个字段需要重点说明一下: 1) 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

2) 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。 3) 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

URG:紧急指针(urgent pointer)有效。

ACK:确认序号有效。

PSH:接收方应该尽快将这个报文交给应用层。

RST:重置连接。SYN:建立一个新连接。

FIN:断开一个连接。

连接的建立(三次握手)

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:

img

客户端调用 socket() 创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。

这个时候,客户端开始发起请求: 1) 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。

2) 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。 服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入SYN-RECV状态。

3) 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。

接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

4) 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

注意:三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。

断开连接(四次挥手)

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。 建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话: [Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”

[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”等待片刻后……

[Shake 3] 套接字B:“我准备好了,可以断开连接了。”

[Shake 4] 套接字A:“好的,谢谢合作。”

下图演示了客户端主动断开连接的场景:

img

 

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求: 1) 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

2) 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。

注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

3) 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。

4) 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。

5) 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。

6) 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

注意:关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢? TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

1.2、UDP协议

UDP(User Datagram Protocol, 用户数据报协议)是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,可以保证通讯效率,传输延时小。例如视频聊天应用中用的就是UDP协议,这样可以保证及时丢失少量数据,视频的显示也不受很大影响。

一、什么是 socket?

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。

socket是什么?

 

 

二、UNIX/Linux 中的 socket 是什么?

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。 UNIX/Linux 中的一切都是文件! 文件描述符(File Descriptor)

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;

  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

我们可以通过 socket() 来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;

  • 用 write() 向远程计算机写入数据。

    你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!

三、socket有哪些类型?

根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 创建连接时,必须告诉它使用哪种数据传输方式。

3.1、流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。 SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

流格式套接字有自己的纠错机制,在此我们就不讨论了。

SOCK_STREAM 有以下几个特征:

  • 数据在传输过程中不会消失;

  • 数据是按照顺序传输的;

  • 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。

可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

将面向连接的套接字比喻成传送带

为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。

 

你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。

假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。

也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。

3.2、数据报格式套接字(SOCK_DGRAM)

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。 计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。

可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:

 

将无连接套接字比喻成摩托车快递

另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。

四、OSI模型

如果你读过计算机专业,或者学习过网络通信,那你一定听说过 OSI 模型,它曾无数次让你头大。OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。 OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

 

OSI 七层网络模型和 TCP/IP 四层网络模型的对比 图1:OSI 七层网络模型和 TCP/IP 四层网络模型的对比

这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

你看,在互联网上传输一份数据是多么地复杂啊,而我们却感受不到,这就是网络模型的厉害之处。我们只需要在代码中调用一个函数,就能让下面的所有网络层为我们工作。

两台计算机进行通信时,必须遵守以下原则:

  • 必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。

  • 每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。

  • 数据只能逐层传输,不能跃层。

  • 每一层可以使用下层提供的服务,并向上层提供服务。

img

 

img

五、socket在Python下的使用

5.1、创建套接字对象

Linux 中的一切都是文件,每个文件都有一个整数类型的文件描述符;socket 也可以视为一个文件对象,也有文件描述符。

import socket

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

# <socket.socket fd=496, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
print(sock)

1、AF 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。大家需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。

2、 type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

3、 protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。本教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() :

# IPPROTO_TCP表示TCP协议
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)  

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() :

# IPPROTO_UDP表示UDP协议
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)  

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) # 创建TCP套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)  # 创建UDP套接字

4、sock = socket.socket()默认创建TCP套接字。

5.2、 套接字对象方法

服务端:bind方法

socket 用来创建套接字对象,确定套接字的各种属性,然后服务器端要用 bind() 方法将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 方法建立连接。

import socket

sock = socket.socket()
sock.bind(("127.0.0.1",8899))

服务端:listen方法

通过 listen() 方法可以让套接字进入被动监听状态,sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 方法的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为SOMAXCONN ,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。当请求队列满时,就不再接收新的请求。

注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

sock.listen(5)

服务端:accept方法

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

conn,addr=sock.accept()

print("conn:",conn) # conn: <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 64915)>

print("addr:",addr) # addr: ('127.0.0.1', 64915)

客户端:connect方法

connect() 是客户端程序用来连接服务端的方法,:

import socket
ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)

注意:只有经过connect连接成功后的套接字对象才能调用.发送和接受方法(send/recv),所以服务端的sock对象不能send or recv。

收发数据方法:send和recv

s.recv()接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。
s.send() 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。

简单聊天示例:

服务端:


import socket

sock = socket.socket()
sock.bind(("127.0.0.1",8899))
sock.listen(5)

while 1:
   client_sock,addr = sock.accept()
   print("客户端%s建立连接"%str(addr))
   while 1:
       try:
           data = client_sock.recv(1024) # data字节串
       except Exception:
           client_sock.close()
           print("客户端%s退出"%str(addr))
           break
       print(data.decode())
       res = input(">>>")
       client_sock.send(res.encode())

客户端:

import socket
ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)


while 1:
   data = input(">>>")
   sk.send(data.encode())
   res = sk.recv(1024)
   print("服务端:%s"%res.decode())

图解socket函数:

img

5.3、socket缓冲区与阻塞

1、socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

TCP套接字的I/O缓冲区示意图

 

这些I/O缓冲区特性可整理如下:

  • I/O缓冲区在每个TCP套接字中单独存在;

  • I/O缓冲区在创建套接字时自动生成;

  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;

  • 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K!

2、阻塞模式

对于TCP套接字(默认情况下),当使用send() 发送数据时: (1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 send() 函数继续写入数据。

(2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,send() 也会被阻塞,直到数据发送完毕缓冲区解锁,send() 才会被唤醒。

(3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

(4) 直到所有数据被写入缓冲区 send() 才能返回。

当使用recv() 读取数据时: (1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

(2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。

(3) 直到读取到数据后 recv() 函数才会返回,否则就一直被阻塞。

TCP套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式,后续我们会讲解。

5.4、粘包(数据的无边界性)

上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,recv() 不管数据发送了多少次,都会尽可能多的接收数据。也就是说,recv() 和 send() 的执行次数可能不同。

例如,send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。

假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。

这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。

import socket
import time

s = socket.socket()
s.bind(("127.0.0.1",8888))
s.listen(5)

client,addr = s.accept()
time.sleep(10)
data = client.recv(1024)
print(data)

client.send(data)
import socket

s = socket.socket()
s.connect(("127.0.0.1",8888))

data = input(">>>")

s.send(data.encode())
s.send(data.encode())
s.send(data.encode())

res = s.recv(1024)

print(res)

5.5、案例之模拟ssh命令

服务端程序:


import socket
import subprocess
import time
import struct

sock = socket.socket()
sock.bind(("127.0.0.1",8899))
sock.listen(5)

while 1:
   client_sock,addr = sock.accept()
   print("客户端%s建立连接"%str(addr))
   while 1:
       try:
           cmd = client_sock.recv(1024) # data字节串
       except Exception:
           print("客户端%s退出"%str(addr))
           client_sock.close()
           break
       print("执行命令:",cmd.decode("gbk"))

       # 版本1:内存问题
       # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
       # client_sock.send(cmd_res_bytes)

       # 版本2:粘包问题
       # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
       # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8")
       # client_sock.sendall(cmd_res_bytes_len)
       # client_sock.sendall(cmd_res_bytes)

       # 版本3:粘包解决方案

       # result_str = subprocess.getoutput(cmd.decode("gbk"))
       # result_bytes = bytes(result_str, encoding='utf8')
       # res_len = struct.pack('i',len(result_bytes))
       # client_sock.sendall(res_len)
       # client_sock.sendall(result_bytes)

       # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
       # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8")
       # res_len = struct.pack('i', len(cmd_res_bytes))
       # client_sock.sendall(res_len)
       # client_sock.sendall(cmd_res_bytes)

客户端程序:

import socket
import time
import struct

ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)


while 1:
   data = input("输入执行命令>>>")
   sk.send(data.encode())

   # 版本1 内存问题
   # res = sk.recv(1024)
   # print("字节长度:",len(res))
   # print("执行命令结果:%s"%(res.decode()))

   # 版本2 粘包问题
   # # time.sleep(5)
   # res_len = sk.recv(1024)
   # data = sk.recv(int(res_len.decode()))
   # print(res_len)
   # print(data.decode())

   # 版本3:粘包解决方案

   # length_msg = sk.recv(4)
   # length = struct.unpack('i', length_msg)[0]
   # msg = sk.recv(length).decode()
   # print("执行命令结果:",msg)

测试命令:

ipconfig
netstat -an

5.6、案例之文件上传

程序的目录结构

`socketDemo``├── client``│   ├── cli.py``│   └── local_dir``│       └── lianxijiangjie.mp4``└── server``    ``├── download``    ``│   └── lianxijiangjie.mp4``    ``└── ser.py`

服务端代码:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'mosson'
import socket
import struct
import json
import os

base_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.join(base_dir, 'download')


class MYTCPServer:
   address_family = socket.AF_INET
   socket_type = socket.SOCK_STREAM
   allow_reuse_address = False
   max_packet_size = 8192
   coding='utf-8'
   request_queue_size = 5
   server_dir='file_upload'

   def __init__(self, server_address, bind_and_activate=True):
       """Constructor. May be extended, do not override."""
       self.server_address=server_address
       self.socket = socket.socket(self.address_family,
                                   self.socket_type)
       if bind_and_activate:
           try:
               self.server_bind()
               self.server_activate()
           except:
               self.server_close()
               raise

   def server_bind(self):
       """Called by constructor to bind the socket.
      """
       if self.allow_reuse_address:
           self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
       self.socket.bind(self.server_address)
       self.server_address = self.socket.getsockname()

   def server_activate(self):
       """Called by constructor to activate the server.
      """
       self.socket.listen(self.request_queue_size)

   def server_close(self):
       """Called to clean-up the server.
      """
       self.socket.close()

   def get_request(self):
       """Get the request and client address from the socket.
      """
       return self.socket.accept()

   def close_request(self, request):
       """Called to clean up an individual request."""
       request.close()

   def run(self):
       print('server is running .......')
       while True:
           self.conn,self.client_addr=self.get_request()
           print('from client ',self.client_addr)
           while True:
               try:
                   head_struct = self.conn.recv(4)
                   if not head_struct:break

                   head_len = struct.unpack('i', head_struct)[0]
                   head_json = self.conn.recv(head_len).decode(self.coding)
                   head_dic = json.loads(head_json)

                   print(head_dic)
                   cmd=head_dic['cmd']
                   if hasattr(self,cmd):
                       func=getattr(self,cmd)
                       func(head_dic)
               except Exception:
                   break

   def put(self,args):
       """
      文件长传
      :param args:
      :return:
      """
       file_path=os.path.normpath(os.path.join(
           base_dir, args['filename']))

       filesize=args['filesize']
       recv_size=0
       print('----->',file_path)
       with open(file_path,'wb') as f:
           while recv_size < filesize:
               recv_data=self.conn.recv(2048)
               f.write(recv_data)
               recv_size += len(recv_data)
           else:
               print('recvsize:%s filesize:%s' %(recv_size,filesize))

   def get(self, args):
       """ 下载文件
      1 检测服务端文件是不是存在
      2 文件信息 打包发到客户端
      3 发送文件
      """
       filename = args['filename']
       dic = {}
       if os.path.isfile(base_dir + '/' + filename):
           dic['filesize'] = os.path.getsize(base_dir + '/' + filename)
           dic['isfile'] = True
       else:
           dic['isfile'] = False
       str_dic = json.dumps(dic) # 字典转str
       bdic = str_dic.encode(self.coding) # str转bytes
       dic_len = len(bdic) # 计算bytes的长度
       bytes_len = struct.pack('i', dic_len) #
       self.conn.send(bytes_len) # 发送长度
       self.conn.send(bdic)  # 发送字典
       # 文件存在发送真实文件
       if dic['isfile']:
           with open(base_dir + '/' + filename, 'rb') as f:
               while dic['filesize'] > 2048:
                   content = f.read(2048)
                   self.conn.send(content)
                   dic['filesize'] -= len(content)
               else:
                   content = f.read(2048)
                   self.conn.send(content)
                   dic['filesize'] -= len(content)
           print('下载完成')


tcpserver1=MYTCPServer(('127.0.0.1',8083))

tcpserver1.run()

客户端代码:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'mosson'

import socket
import struct
import json
import os
import time

base_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.join(base_dir, 'local_dir')


class MYTCPClient:
   address_family = socket.AF_INET

   socket_type = socket.SOCK_STREAM

   allow_reuse_address = False

   max_packet_size = 8192

   coding = 'utf-8'

   request_queue_size = 5

   def __init__(self, server_address, connect=True):
       self.server_address = server_address
       self.socket = socket.socket(self.address_family,
                                   self.socket_type)
       if connect:
           try:
               self.client_connect()
           except:
               self.client_close()
               raise

   def client_connect(self):
       self.socket.connect(self.server_address)

   def client_close(self):
       self.socket.close()

   def run(self):
       while True:
           inp = input(">>: ").strip()
           if not inp: continue
           l = inp.split()
           cmd = l[0]
           if hasattr(self, cmd):
               func = getattr(self, cmd)
               func(l)

   def put(self, args):
       cmd = args[0]
       filename = args[1]
       filename = base_dir + '/' + filename
       print(filename)
       if not os.path.isfile(filename):
           print('file:%s is not exists' % filename)
           return
       else:
           filesize = os.path.getsize(filename)

       head_dic = {'cmd': cmd, 'filename': os.path.basename(filename), 'filesize': filesize}
       print(head_dic)
       head_json = json.dumps(head_dic)
       head_json_bytes = bytes(head_json, encoding=self.coding)

       head_struct = struct.pack('i', len(head_json_bytes))
       self.socket.send(head_struct)
       self.socket.send(head_json_bytes)
       send_size = 0
       t1 = time.time()
       # with open(filename,'rb') as f:
       #     for line in f:
       #         self.socket.send(line)
       #         send_size+=len(line)
       #     else:
       #         print('upload successful')
       #         t2 = time.time()
       with open(filename, 'rb') as f:
           while head_dic['filesize'] > 2048:
               content = f.read(2048)
               self.socket.send(content)
               head_dic['filesize'] -= len(content)
           else:
               content = f.read(2048)
               self.socket.send(content)
               head_dic['filesize'] -= len(content)
           t2 = time.time()

       print(t2 - t1)

   def get(self, args):
       cmd = args[0]
       filename = args[1]
       dic = {'cmd': cmd, 'filename': filename}
       """发送dic的步骤
      字典转str
      str转bytes
      计算bytes的长度
      发送长度
      发送字典
      """
       str_dic = json.dumps(dic)  # 字典转str
       bdic = str_dic.encode(self.coding)  # str转bytes
       dic_len = len(bdic)  # 计算bytes的长度
       bytes_len = struct.pack('i', dic_len)  #
       self.socket.send(bytes_len)  # 发送长度
       self.socket.send(bdic)  # 发送字典

       # 接受 准备下载的文件信息
       dic_len = self.socket.recv(4)
       dic_len = struct.unpack('i', dic_len)[0]
       dic = self.socket.recv(dic_len).decode(self.coding)
       dic = json.loads(dic)
       # 文件存在准备下载
       if dic['isfile']:
           t1 = time.time()
           with open(base_dir + '/' + filename, 'wb') as f:
               while dic['filesize'] > 2048:
                   content = self.socket.recv(2048)
                   f.write(content)
                   dic['filesize'] -= len(content)
               else:
                   while dic['filesize']:
                       content = self.socket.recv(2048)
                       f.write(content)
                       dic['filesize'] -= len(content)
                   t2 = time.time()
           print(t2 - t1)

       else:
           print('文件不存在!')


client = MYTCPClient(('127.0.0.1', 8083))

client.run()

作业:开发一个FTP程序

# 要求
1、允许上传和下载文件,保证文件一致性
2、文件传输过程中显示进度条
3、附加功能:支持文件的断点续传

 

posted @ 2020-10-24 17:16  lxingchen  阅读(186)  评论(0编辑  收藏  举报