昨日内容回顾

网络的基础概念

arp 协议 :通过 ip 地址找到 mac 地址

五层模型 : 应用层 传输层 网络层 数据链路层 物理层

tcp 协议 : 可靠的 面向连接 全双工

三次握手

四次挥手

udp 协议 : 不可靠的 面向数据包的 高效的

socket:

是模块 是和应用层直接交互,

向下封装了,应用层之下的相关协议的一组网络通信的接口

全双工,表示双向连接。

为什么四次挥手的 2 次连接,不可以合并?

第一次断开,不会立即断开,如果还有数据,可以发送。所以不可以合并。

python 代码,属于应用层

socket 网络通信

新建文件 server.py,内容如下:

  1. import socket
  2. sk = socket.socket()
  3. sk.bind(('127.0.0.1',9000))
  4. sk.listen() # 参数n表示同一时间可以有n个链接等待与server段通信
  5. conn,addr = sk.accept()
  6. ret = conn.recv(1024).decode('utf-8')
  7. print(ret)
  8. conn.send(b'hello')
  9. conn.close()
  10. sk.close()

新建文件 client.py,内容如下:

  1. import socket
  2. sk = socket.socket()
  3. sk.connect(('127.0.0.1',9000))
  4. sk.send(b'hello')
  5. print(sk.recv(1024).decode('utf-8'))
  6. sk.close()

先执行 server.py,再执行 client.py,输出:

hello

但这是一次性的,能不能 server 与 client 交互呢?

修改 server.py,内容如下:

  1. import socket
  2. sk = socket.socket()
  3. sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
  4. sk.bind(('127.0.0.1',9000))
  5. sk.listen() # 参数n表示同一时间可以有n个链接等待与server端通信
  6. while True:
  7. conn,addr = sk.accept()
  8. while True:
  9. ret = conn.recv(1024).decode('utf-8')
  10. if ret == 'q':break
  11. print(ret)
  12. inp = input('>>>')
  13. conn.send(inp.encode('utf-8'))
  14. if inp == 'q':break
  15. conn.close()
  16. sk.close()

修改 client.py,内容如下:

  1. import socket
  2. sk = socket.socket()
  3. sk.connect(('127.0.0.1',9000))
  4. while True:
  5. inp = input('>>>')
  6. sk.send(inp.encode('utf-8'))
  7. if inp == 'q':break
  8. ret = sk.recv(1024).decode('utf-8')
  9. if ret == 'q':break
  10. print(ret)
  11. sk.close()

先执行 server.py,再执行 client.py,效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973477545-3a0ade55-a872-4201-90fc-70cd42a06d03.png)

server 可以主动关闭 client,如果 client 再次开启,还可以继续聊。

如果 client 和 server 已经建立连接了。这个时候,再来一个客户端,新来的就不能用了。为啥呢?

对于一个 tcp 连接,客户端和 server 是一直占用的。其他所有客户端,全都不能占用。

因为连接是阻塞的。

如果一个 server 没有执行 sk.colse(),再开一个进程时,就会出现

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973477659-51a08f4d-0c8f-4935-9c38-97d84f3358b0.png)

解决方案:

  1. sk = socket.socket()
  2. sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 表示允许端口复用
  3. sk.bind(('127.0.0.1',9000))

总结:

tcp 协议适用于 文件的上传和下载 发送邮件 发送重要的文件

每和一个客户端建立链接 都会在自己的操作系统上占用一个资源

同一时间 只能 和一个客户端通信

数据交互是从上至下,然后再从下至上

下图,表示 2 台机器

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973477605-9c6ec895-fea6-4454-b585-5f656b15401e.png)
  2. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973477582-c94d18c1-0779-4adc-b9a5-9a1981c470dc.png)

基于 UDP 协议的 socket

udp 是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接

简单使用

server 端

  1. import socket
  2. sk = socket.socket(type=socket.SOCK_DGRAM)
  3. sk.bind(('127.0.0.1',9000))
  4. msg,client_addr = sk.recvfrom(1024) # udp协议不同建立链接
  5. print(msg)
  6. sk.sendto(b'world',client_addr)
  7. sk.close()

client 端

  1. import socket
  2. sk = socket.socket(type=socket.SOCK_DGRAM)
  3. sk.sendto(b'hello',('127.0.0.1',9000))
  4. ret = sk.recvfrom(1024)
  5. print(ret)
  6. sk.close()

先执行 server.py,再执行 client.py,执行输出:

(b’world’, (‘127.0.0.1’, 9000))

server.py,输出:b’hello’

上面的代码,执行一次就结束了,改成多次执行的。

server.py 内容如下:

  1. import socket
  2. sk = socket.socket(type=socket.SOCK_DGRAM)
  3. sk.bind(('127.0.0.1',9090))
  4. while True:
  5. msg,client_addr = sk.recvfrom(1024)
  6. print(msg.decode('utf-8'))
  7. inp = input('>>>')
  8. sk.sendto(inp.encode('utf-8'),client_addr)
  9. sk.close()

client.py 内容如下:

  1. import socket
  2. sk = socket.socket(type=socket.SOCK_DGRAM)
  3. while True:
  4. inp = input('>>>').strip()
  5. sk.sendto(inp.encode('utf-8'),('127.0.0.1',9090))
  6. msg,addr = sk.recvfrom(1024)
  7. print(msg.decode('utf-8'))
  8. sk.close()

在 udp 中,数据必须是 server 端先接收

先执行 server.py,再执行 client.py,效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973477605-713db4e1-cf95-41f7-8465-c7bbb206262e.png)

client 和 server 可以相互通信。如果再新开一个 client,也是可以和 server 通信的。

那么问题来了,如果 2 个 client,同时发给 server,server 先不回复。

最后再回复时,server 会回复给谁呢?

在网络中,是有先后顺序的。即是是 0.0001 秒的差距,谁最后给 server,那么 server 就会回复给它。

再增加一个功能,显示人名。

修改 client.py,内容如下:

  1. import socket
  2. sk = socket.socket(type=socket.SOCK_DGRAM)
  3. name = input('请输入名字: ')
  4. while True:
  5. inp = input('请输入发送内容: ')
  6. sk.sendto(('%s : %s'%(name,inp)).encode('utf-8'),('127.0.0.1',9090))
  7. msg,addr = sk.recvfrom(1024)
  8. print(msg.decode('utf-8'))
  9. sk.close()

server.py 不需要修改。先执行 server.py,再执行 client.py,效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973478028-2921d11c-98f3-4f9d-b494-79aba3017bf7.png)

再增加一个功能,人名显示不同的颜色。

修改 server.py,内容如下:

  1. import socket
  2. lst = {'egon':'\033[1;31m','yuan':'\033[1;34m'}
  3. sk = socket.socket(type=socket.SOCK_DGRAM)
  4. sk.bind(('127.0.0.1',9090))
  5. while True:
  6. msg,client_addr= sk.recvfrom(1024) # udp协议不用建立链接
  7. name,mesg = msg.decode('utf-8').split(':')
  8. color = lst.get(name.strip(),'')
  9. print('%s%s\033[0m'%(color,msg.decode('utf-8')))
  10. inp = input('>>>')
  11. sk.sendto(inp.encode('utf-8'),client_addr)
  12. sk.close()

client.py 不需要修改,先执行 server.py,再执行 client.py,效果如下:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973478031-5a4def14-540b-4ffc-83e6-e0c8ac2a006e.png)

切换到 eva 和 yuan,就会有不同的颜色显示。

现在有一个需求,utf-8 转码很麻烦,能不能自动转换呢?

是可以的,写一个类,这个类继承了 socket,它能满足定制需求。

新建一个文件 mysocket.py,内容如下:

  1. from socket import * # 导入socket模块
  2. class Mysocket(socket): # 继承socket
  3. def __init__(self,coding='utf-8'): # 默认编码为utf-8
  4. self.coding = coding
  5. super().__init__(type=SOCK_DGRAM) # 设定为udp协议
  6. def my_recv(self,num): # num表示最大字节,比如1024
  7. msg,addr = self.recvfrom(num)
  8. return msg.decode(self.coding),addr # 返回解码后的接收信息
  9. def my_send(self,msg,addr): # msg和addr分别表示发送信息和连接ip:端口
  10. return self.sendto(msg.encode(self.coding),addr) # 发送编码后的信息

修改 server.py,内容如下:

  1. from mysocket import Mysocket
  2. sk = Mysocket() # 可以指定编码,默认为utf-8
  3. lst = {'eva':'\033[1;31m','yuan':'\033[1;34m'}
  4. sk.bind(('127.0.0.1',9090))
  5. while True:
  6. msg,client_addr= sk.my_recv(1024) # udp协议不用建立链接
  7. name,mesg = msg.split(':')
  8. color = lst.get(name.strip(),'')
  9. print('%s%s\033[0m'%(color,msg))
  10. inp = input('>>>')
  11. sk.my_send(inp,client_addr)
  12. sk.close()

client.py,内容如下:

  1. from mysocket import Mysocket
  2. sk = Mysocket()
  3. name = input('请输入名字: ')
  4. while True:
  5. inp = input('请输入发送内容: ')
  6. sk.my_send(('%s : %s'%(name,inp)),('127.0.0.1',9090))
  7. msg,addr = sk.my_recv(1024)
  8. print(msg)
  9. sk.close()

先执行 server.py,再执行 client,py。执行效果同上!

时间同步服务

基于 udp 协议完成的

公司有 4,5 台服务器,00:00 执行任务,从一个文件里面 读取一些数据,作为明天的搜索关键字。

假如有一台服务器,慢了 1 小时,那么就会影响客户端访问。

现在机房里面,有一台标准时间的服务器。

机房里所有的机器,每隔一段时间,就去请求这台服务器,来获取一个标准时间

比如一个有标准时间的 server,client 可以从 server 获取标准时间,需要使用 udp 协议。

server 不能关,它必须先接收客户端的 ip,端口,信息,它才能将新消息,发给指定的人。

修改 server.py,内容如下:

  1. import time
  2. import socket
  3. sk = socket.socket(type = socket.SOCK_DGRAM)
  4. sk.bind(('127.0.0.1',9090))
  5. while True:
  6. msg,addr = sk.recvfrom(1024)
  7. sk.sendto(time.strftime(msg.decode('utf-8')).encode('utf-8'),addr)
  8. sk.close()

修改 client.py,内容如下:

  1. import time
  2. import socket
  3. sk = socket.socket(type=socket.SOCK_DGRAM)
  4. while True:
  5. sk.sendto('%Y/%m/%d %H:%M:%S'.encode('utf-8'),('127.0.0.1',9090)) # 执行时间格式
  6. ret,addr = sk.recvfrom(1024)
  7. print(ret.decode('utf-8'))
  8. time.sleep(1) # 暂停1秒执行
  9. sk.close()

先执行 server.py,再执行 client,py,执行输出:

2018/05/04 16:33:08

2018/05/04 16:33:09

2018/05/04 16:33:10

在真正的工作中,不会让 client 一直 whilte true 的

是用 linux 任务计划执行的

socket 参数的详解

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

创建 socket 对象的参数说明:

family 地址系列应为 AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN 或 AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为 SOCKSTREAM(默认值),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()关闭一个独立的插座。

最主要用到的参数是 type

看下图:

  1. ![](https://cdn.nlark.com/yuque/0/2020/png/1484428/1597973478054-bc463800-bc35-4367-a7c8-fb2836ce4f5f.png)

左边是 TCP,右边是 UDP

对比可以看出,UDP 的步骤比较少一点。

熟练掌握以下 3 个例子

时间同步

tcp 协议的聊天

udp 协议的聊天

周一默写 重写 socket 类的那一套,有 3 个文件。

server.py,内容如下:

  1. from mysocket import Mysocket
  2. sk = Mysocket()
  3. sk.bind(('127.0.0.1',9090))
  4. while True:
  5. msg,client_addr= sk.my_recv(1024) # udp协议不用建立链接
  6. print(msg)
  7. if msg == 'q':sk.close()
  8. inp = input('>>>')
  9. sk.my_send(inp,client_addr)
  10. if inp == 'q':sk.close()
  11. sk.close()

mysocket.py,内容如下:

  1. from socket import *
  2. class Mysocket(socket):
  3. def __init__(self,coding='utf-8'):
  4. self.coding = coding
  5. super().__init__(type=SOCK_DGRAM)
  6. def my_recv(self,num):
  7. msg,addr = self.recvfrom(num)
  8. return msg.decode(self.coding),addr
  9. def my_send(self,msg,addr):
  10. return self.sendto(msg.encode(self.coding),addr)

client.py,内容如下:

  1. from mysocket import Mysocket
  2. sk = Mysocket()
  3. while True:
  4. inp = input('>>>')
  5. sk.my_send(inp,('127.0.0.1',9090))
  6. msg,addr = sk.my_recv(1024)
  7. print(msg)
  8. sk.close()

周末作业:

1.继续完成聊天 使用udp协议

加上重写socket类 + 名字 + 颜色

进阶功能:

1.添加好友,删除好友。

2.client简单登录,用来表明身份,以登录用户来聊天。

2.cs架构的程序的登陆

ftp做准备

hashilib密文 client端简单加密一次 + server端加密

使用tcp协议

进阶功能:

1.远程创建文件夹

第一个作业,请参考文章

http://www.py3study.com/Article/details/id/237.html

第二个作业,请参考文章

http://www.py3study.com/Article/details/id/238.html