python基础之socket编程1

一 客户端/服务器架构

1.硬件C/S架构(打印机)

2.软件C/S架构

  互联网中处处是C/S架构。最常用的软件服务器就是web服务器。

  如黄色网站是服务端,你的浏览器是客户端(B/S架构也是C/S架构的一种)

  腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频)

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

二 OSI七层

引子:

须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)

如果你要跟别人一起玩,那你就需要上网了,什么是互联网?

互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语

如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

人们按照分工不同把互联网协议从逻辑上划分了层级,

详见网络通信原理:http://www.cnblogs.com/linhaifeng/articles/5937962.html

 

为何学习socket一定要先学习互联网协议:

1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件

2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的

3.然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。

4.最后:就让我们从这些标准开始研究,开启我们的socket编程之旅

                                                                              

三 socket层

在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

四 socket是什么

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

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序 而程序的pid是同一台机器上不同进程(一个程序可以有多个进程)或者线程的标识。

五 套接字发展史及分类

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

六 套接字工作流程

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

服务端:                                                                 客户端:

1、买电话   socket()              1、拨电话connect()

2、绑定一个手机卡  bind()            2、发消息read()  

3、开机   listen()                 3、收消息write()

4、等电话,拿到一个电话连接   accept()     4、关机close()

5、收read()  

6、发write()消息

7、断开电话连接

8、关机   close()

      

                                           图3       

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

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 
11 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
12 例如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三次握手和四次挥手

四次挥手:

主动断开链接由客户端发起时:

1、客户端发一个FIN断开链接的请求,客户端进入FIN_WAIT_1"主动断开链接"状态

2、服务端回一个ACK,从而客户端向服务端发数据的就断开了

3、服务端发FIN断开请求,客户端进入FIN_WAIT_2状态(被动断开链接)和TIME_WAIT状态(马上就要断开连接了,就差一次ACK了)

4、客户端回一个ACK,从而服务端向客户端发数据的就断开了

注意:1、主动断开链接也可以由服务端发起,这取决于为什么要断链接,即谁发送完数据就会主动发起断开链接

2、为什么建立链接三次,断开四次?

在建立链接的时候,还没有任何数据的传输,只是单纯地建立链接。

而断开链接的时候,有可能另外一条链接上还存在数据的传输。

例如,客户端像服务端发数据的时候,数据发完了,客户端就可以申请断开链接FIN。此时如果是三次握手服务端返回客户端ACK的同时也得返回被动断开链接的FIN请求,最后客户端回一个ACK给服务端。此机制下,如果服务端还有正在向客户端传数据,客户端只要一发断开链接的请求,在第二步中服务端就会被动地断开服务端向客户端发消息的请求。此时还在传输的数据就丢失了。

3、大并发情况:

同时有很多客户同时访问一个服务器的时候,每个客户端都要和服务器建立一个TCP/IP连接。这些连接都保存在服务器上。

首先发起主动断开链接方才会有TIME_WAIT状态,大并发情况下会有大量的TIME_WAIT,因为服务端根本不会保留大量的与客户端连接,会主动断开链接,为了节省资源。所以一般实际中都是服务端主动断开。

如果运行结束了,间隔很短立即重启建立链接,可能会报错:“地址正在被使用”,因为服务端此时发送的最后一个ACK还没有传到客户端,服务端还处于TIME_WAIT状态,地址还在被使用。

 
       
#服务端:虚拟机模拟
import socket
#买手机:
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
#AF_INET:本socket基于网络通信,SOCK_STREAM:基于TCP/IP协议(流通信)
#绑定手机卡:
phone.bind(('192.168.12.222',8001))   #自己的IP地址和端口
#服务端监听的IP地址
#写谁的IP地址就要在哪台机器上运行这个服务端程序
phone.listen(5) #开机   #参数5表示最多可以接多少个电话在等
# 5就是backlog值,半连接池的大小。服务端还会自己进行调节
print('---->') #等电话:建立链接的过程(三次握手) conn,addr=phone.accept() #元祖形式:(得到电话连接conn,对方的手机号addr)

#accept相当于拿到了一个TCP链接conn(TCP为双向链接),建立好了之后才可以进行数据传输
#因此链接好了之后服务端和客户端互相都可以发消息了

#数据传输阶段:
#
收消息 msg=conn.recv(1024) #1024表示收了多少字节的信息 print('客户端发来的消息是: ',msg) #发消息(先当做收发消息发的是二进制,因此发的时候要编码) conn.send(msg.upper()) #将收的msg编程全大写之后返回去
#断开链接(四次挥手)
#
挂电话 conn.close() #关机:关闭socket phone.close() #运行结果:客户端发来的消息是: b‘hello’
#客户端
import  socket
#买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#拨通电话(accept在等电话)
phone.connect(('192.168.12.222',8001))   
# 客户端写的IP地址:你想要通信的服务端正在的监听的IP
#发消息给服务端(服务端的recv来接收)
phone.send('hello'.encode('utf-8'))   #发的时候要编码,只认识二进制,否则报错,变bytes格式
#收服务端发来的消息
data=phone.recv(1024)
print('收到服务端的发来的消息:',data)

#运行结果:收到服务端的发来的消息:b‘HELLO’

 

#实践中错误:
1.文件名不要命名为模块或者包的名字,否则你import的时候会错误地import自己这个文件了
2.重启的时候报错:
phone.bind(('127.0.1.11',8000))
“通常每个套接字地址(协议/网络地址/端口)只允许用一次”

报此错的原因是:bind在绑定一个ip地址和端口号的时候,你的机器会监听一个端口号。
当你把程序关闭的时候,机器不会立即将此监听端口号关闭,没有立即清除掉。
在Mac笔记本中很常见。

 

3、socket的底层工作原理:
SYN洪水攻击(基于TCP/IP的漏洞):
假的客户端全发起通信,服务端全要回应大量的SYN,多次回复等待回应。占用大量资源
backlog:半链接池。服务端可能有很多客户端,没办法一次性全立刻响应。
        服务端将所有的SYN先保存在半链接池中,每次从这里面取出来。
backlog的好处:1、防止洪水攻击   2、缩小返回SYN的次数
backlog对应服务端程序中的listen参数

4、TCP/IP是一种可靠链接的原因:
数据不会传丢,收到数据后会回复给发送端表示收到了。如果没收到会重新发一个。

 七、 修改升级版:服务端循环链接请求来收发消息

 #服务端
# import socket
# 可以在导入socket的时候,将导入写法改写为:
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)   
#改写import方式之后,在访问的时候就不用加上socket. 来访问了
tcp_server.bind(ip_port)
tcp_server.listen(back_log)


# 为实现客户端服务端循环收发消息:
# 最外层while循环的目的:为多个用户提供服务:来一个链接就接收一次
while True: print('服务端开始运行了') conn,addr=tcp_server.accept() #服务端阻塞,等待一个来自客户端的TCP/IP链接 # 当客户端与服务端成功建立起链接的时候,accept才会运行,从而打印下面的信息: print('双向链接是',conn) #conn为套接字对象 # <socket.socket fd=224, family=AddressFamily.AF_INET,type=SocketKind.SOCKET_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 49863)> print('客户端地址',addr) #同一台电脑上打印出的这个客户端的addr与服务端的一样
#这个内层的while循环,进入与某一个客户端之间的收发消息
while True: # try: #加入异常处理,为了解决和第一个客户端成功断开连接 data=conn.recv(buffer_size) print('客户端发来的消息是',data.decode('utf-8')) conn.send(data.upper()) #将收到的数据全变为大写 # except Exception: # break conn.close() tcp_server.close()
#客户端
# import socket
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
#此时已经成功建立起链接了,服务端会打印已经建立链接的信息

while True:
    msg=input('>>: ').strip()   #让用户自己输入字符串
    if not msg:continue
    tcp_client.send(msg.encode('utf-8'))        #对发送的消息进行编码
    print('客户端已经发送消息')
    data=tcp_client.recv(buffer_size)
    print('收到服务端发来的消息',data.decode('utf-8'))  #对收到的字节解码

tcp_client.close()

注意:

1、recv(1024)并不意味着你就能收到1024,如果自己的内核态缓存区中只有1个字节,那就取一个。

  如果发到缓存区的数据大于1024,那一次只能取一部分,剩下的进入缓存队列中(先进先出)

2、如果客户端程序直接强行终止,服务端也会断开。服务端的屏幕上抛出异常:

  “ConnectionResetError:[WinError 10054] 远程主机强迫关闭了一个现有的连接”

  因为此时服务端还在等着conn.recv,结果conn没了,因此抛出异常。因此加入异常处理try-except。

3、两个客户端先后向服务端收发消息的过程:

第一个客户端先和服务端建立起连接,并成功向服务端收发了几条消息。

第二个客户端也开始运行并发送几条消息给服务端,此时此连接请求到服务端的连接池backlog中被挂起了。

此时服务端还在和第一个客户端通信呢,因此此客户端只能等服务端终止与第一个客户端之间的通信后,才能来响应客户端2的请求,进而才能完成三次握手。

4、用虚拟机来模拟上一点中描述的情形:虚拟机中ifconfig来查看本机的IP地址。并将程序中监听的IP地址修改为本机的IP地址。

如果没办法判断出收的是空,可以在data=conn.recv(buffer_size)语句后面加一条if判断语句:if not data:break。就不用try-except来解决了。

八、 socket收发消息原理剖析

socket收发消息的原理图:

socket收发消息剖析
1)在不加if not msg:continue时,如果用户输入了空,客户端仍会执行发送操作
2)此时客户端发送的消息先进入客户端自己的内核态缓存中,然后再传给自己的网卡,
3)由于发送的是空,发送方不会将内核态中的空传给网卡,因此没办法到接收方
4)接收方自己方的内核态缓存中什么都没有,持续等待
5)客户端在打印了'客户端已经发送消息'后,程序卡在了'data=tcp_client.recv(buffer_size)'的位置
6)服务端会执行接收操作data=conn.recv(buffer_size),但此时服务端会卡住
因此,两者都卡在recv当中

 

内存分为:内核态和用户态

1)操作系统启动流程:开机,BIOS程序找启动盘,从启动盘找一段操作系统的内核代码到内存中的内核态部分。
CPU执行在用户态,执行用户自己的程序。因此OS只要在运行,它的内核代码就会在操作系统中

2)计算机系统的三层结构:
应用软件:用户态内存
OS:内核态内存
硬件:发给网卡

因此发消息的时候,
(1)先从发送方用户态中的应用程序发起的send(msg)传给发送方自己的内核态缓存中,
(2)由内核态中的OS操作底层硬件,然后通过发送方网卡传给接收方的网卡,
(3)然后通过接收方的网卡传递给接收方OS的内核态
(4)接收方用户态中的应用程序发起recv(1024),向接受方自己的内核态中要数据
操作系统按照你规定的协议(TCP/IP或者UDP协议)收发消息。

 九、总结:基于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()            # 关闭客户套接字

 

socket通信流程与打电话流程类似,我们就以打电话为例来实现一个low版的套接字通信

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
ip_port=('127.0.0.1',9000)  #电话卡
BUFSIZE=1024                #收发消息的尺寸
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
s.bind(ip_port) #手机插卡
s.listen(5)     #手机待机


conn,addr=s.accept()            #手机接电话
# print(conn)
# print(addr)
print('接到来自%s的电话' %addr[0])

msg=conn.recv(BUFSIZE)             #听消息,听话
print(msg,type(msg))

conn.send(msg.upper())          #发消息,说话

conn.close()                    #挂电话

s.close()                       #手机关机
服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #拨电话

s.send('linhaifeng nb'.encode('utf-8'))         #发消息,说话(只能发送字节类型)

feedback=s.recv(BUFSIZE)                           #收消息,听话
print(feedback.decode('utf-8'))

s.close()                                       #挂电话
客户端

加上链接循环与通信循环

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
ip_port=('127.0.0.1',8081)#电话卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
s.bind(ip_port) #手机插卡
s.listen(5)     #手机待机


while True:                         #新增接收链接循环,可以不停的接电话
    conn,addr=s.accept()            #手机接电话
    # print(conn)
    # print(addr)
    print('接到来自%s的电话' %addr[0])
    while True:                         #新增通信循环,可以不断的通信,收发消息
        msg=conn.recv(BUFSIZE)             #听消息,听话

        # if len(msg) == 0:break        #如果不加,那么正在链接的客户端突然断开,recv便不再阻塞,死循环发生

        print(msg,type(msg))

        conn.send(msg.upper())          #发消息,说话

    conn.close()                    #挂电话

s.close()                       #手机关机
服务端改进版
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #拨电话

while True:                             #新增通信循环,客户端可以不断发收消息
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    s.send(msg.encode('utf-8'))         #发消息,说话(只能发送字节类型)

    feedback=s.recv(BUFSIZE)                           #收消息,听话
    print(feedback.decode('utf-8'))

s.close()                                       #挂电话
客户端改进版

 

问题:

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

这个是由于你的服务端仍然存在四次挥手的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前加:为套接字设置(配置)选项option参数:重新使用IP地址
phone.bind(('127.0.0.1',8080))    #绑定ip地址和端口
#方法二:
发现系统存在大量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 时间

 

 

 

转载于:https://www.cnblogs.com/Josie-chen/p/8919261.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值