2.1网络编程
2.1.1基本概念
2.1.1.1查看IP地址
- Ubuntu下查看IP地址及相关操作
- 关闭网卡:sudo ifconfig 网卡名 down
- 快捷键操作:ctrl+a:快速回到行首;ctrl+e:快速回到行尾
- 开启网卡:sudo ifconfig 网卡名 up
2.1.1.2IPv4和IPv6的介绍
- IPv4和IPv6
- IPv4: 类似于192.168.17.123(共有256的4次方个IP地址)
- 说明:伴随着互联网的快速发展,IPv4已经不能满足日益增长的用户需求,IPv6就是来解决这个问题的
- IPv6:号称地球上的任意一个沙子都可以用IPv6地址唯一标识但
- 说明:是由于如果大范围的使用IPv6会导致许多东西的修改(例如运营商要重新搭建运营网络,这意味着要花费大量的人力和物力,这是很多企业排斥的;再者中国的IPv4地址尚且还没到枯竭的时候,因此目前中国仍然在使用IPv4地址二没有大范围使用IPv6地址
- IPv4地址解析
- 192.168.33.102:这是一个C类IP地址,其中,192.168.33属于网络号而后面的102属于主机号,一般情况下,网络号相同的属于同一个局域网而主机号用来区分一个局域网下的不同主机,但一般情况下主机号为0和为255的不能用;但网络号并不是一定是三组,也可以是两组和一组,根据网络号的组数不同可以将IPv4地址分为A类地址,B类地址,C类地址,分别对应1组网络号,2组网络号,3组网络号
- 私有IP地址(这些IP地址不能用于公网只能用于局域网,所有的局域网内都可以用,可以重复使用)
- 10.0.0.0~10.255.255.255
- 172.16.0.0~172.31.255.255
- 192.168.0.0~192.168.255.255
- 待定
2.1.1.3端口分类
- 知名端口:公众所知的端口号,范围从0到1023
- 88端口分配给http服务
- 21端口分配给ftp服务
- 动态端口:范围从1024到65535
- 说明:两个计算机想要进行通信,IP地址和端口号是缺一不可的,IP地址用来确定主机而端口号用来确定应用程序
2.1.1.4socket简介(套接字)
-
说明:现在几乎所有的网路通信都使用socket,socket是用来完成网络通信所必备的东西
-
在python中使用socket模块的函数socket来实现套接字
import socket socket.socket(AddressFamily Type) #AddressFamily:用来说明你使用的IP地址是IPv4还是IPv6 #Type:用来说明建立的是tcp连接还是udp连接
-
创建一个tcp socket
import socket#由于直接导入模块,后面在调用模块内部的方法时就需要加入socket来调用该方法,像socket.方法名 # from socket import *:表示导入socket这个模块的方法,后面在使用时可以直接写这些方法的名字来调用这些方法而不需要写socket.方法名来调用该方法 # 创建tcp的套接字 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET表明时IPv4地址,SOCK_STREAM表明是tcp连接 #s返回的是一个对象 #使用套接字 #关闭套接字 s.close()
-
创建一个udp socket
import socket#由于直接导入模块,后面在调用模块内部的方法时就需要加入socket来调用该方法,像socket.方法名 # from socket import *:表示导入socket这个模块的方法,后面在使用时可以直接写这些方法的名字来调用这些方法而不需要写socket.方法名来调用该方法 # 创建udp的套接字 s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #AF_INET表明时IPv4地址,SOCK_DGRAM表明是udp连接 #s返回的是一个对象 #使用套接字 #关闭套接字 s.close()
-
使用sublime编写python程序后保存后在终端下直接执行python代码
-
问题一:代码提示不全/导入模块后无相关模块的方法提示
首先:在编辑界面中按下ctrl+shift+p;接着输入install package回车;紧接着再输入SublimeCodeIntel回车;等待安装完成(会有一个安装成功的界面),退出后重启sublime即可(这里可能重启后短时间内无效,等待一会后即可生效)
-
小技巧:使用vim编写程序时,按住esc键保证编辑器处于命令行格式下而不是编辑格式下,按下shift+v键可以选中当前行,然后使用上下箭头可以选中连续的多行文本,按住shift+<可以对本行进行向左缩进,按住shift+>可以对本行进行向右缩进;按住shift+4切换到行尾,按住shift+6切换到行首
-
2.1.2udp
2.1.2.1udp发送数据与接受数据
-
在Mac上使用udp套接字向Ubuntu虚拟机发送信息
-
输入代码:
import socket def main(): udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_socket.sendto(b"hahaha",('192.168.93.132',8080)) #hahaha是要通信的内容,192.168.93.132和8080分别是要通信的IP地址和端口号,前面的b表示字节(注意:这里必须为字节不能为字符串格式) udp_socket.close() if __name__ == '__main__': main()
-
常见问题总结:如果出现连接不通且在Mac中pingUbuntu不通时可以考虑可能是双方不在同一个网络号下,可以通过查看双方的IP地址进行验证,并将虚拟机的网络设置为桥接模式,这样过一会之后你就会发现双方的IP地址在同一个网络号下
-
使用type()可以查看数据的类型
type(123) #int type("123") #str type(b"123") #bytes
-
-
发送udp数据练习
-
在发送端编辑代码
#导入套接字socket所需要的包 import socket def main(): #创建一个udp的套接字 udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 绑定一个端口,如果不绑定端口,计算机会随机分配一个端口且每次执行该程序分配的端口都可能不同 udp_socket.bind(("",9090)) # 注意:bind后面必须是一个元祖,第一个括号表示函数,第二个括号表示元祖(元祖用括号表示),不同主机可以用同一个端口但同一台主机不能用两个相同的端口 while True: #从键盘获取数据 send_data = input("请输入要发送的数据:") #input中的数据是以字符串的形式存储的 #如果输入的是exit就退出循环程序 if send_data == "exit": break #向目标程序发送信息 udp_socket.sendto(send_data.encode("utf-8"),('192.168.93.132',8080))#使用encode("utf-8")编码方式可以解决数据类型不同的问题 #关闭套接字 udp_socket.close() if __name__ == '__main__': main()
-
小技巧:在执行无线循环程序时,可以按住ctrl+z键来强制退出循环
-
-
接受udp数据练习
-
在接收端编写代码
#创建套接字 import socket def main(): udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #绑定一个本地信息 localaddr = ("",8080)#localaddr是一个元祖,分别表示ip地址和端口号,其中ip地址一般不写表示本机的任何一个ip地址,端口号可以写大于1024且小于655345的任意一个,注意这里的ip地址必须是本机的ip地址,不能是其他主机的ip地址 udp_socket.bind(localaddr)#括号内的内容必须是一个元祖 while True: # 重复接受数据 #接受信息 recv_data = udp_socket.recvfrom(1024)#1024表示本次接受的最大字节数,recvfrom是receive from的简称,recvfrom接受的是一个元祖,元祖中包含两个部分:发送的信息以及发送方的信息 #打印接受到的数据 # 解析接受到的信息 recv_msg = recv_data[0] # 存储发送方发送的信息 send_addr = recv_data[1] # 存储发送方的信息 print("%s:%s" % (str(send_addr),recv_msg.decode("utf-8"))) # 将解析的信息均以字符串的形式输出,两个信息之间用:隔开,地址是一个元祖使用str强制转化为字符串的格式,信息发送方是以utf-8的形式编码的所以要用utf-8的形式解码,注意:如果是在windows下用软件发送的信息则是默认使用的是gbk的形式进行编码的所以在也需要使用gbk的形式解码 #关闭套接字 udp_socket.close() if __name__ == "__main__": main()
-
小技巧:在使用vim编辑程序时如何永久的显示行号:直接在终端中输入vim ~/.vimrc(该文件一般默认不存在),然后输入set nu保存退出即可
-
2.1.2.2端口绑定问题
- 对于发送方而言可以不绑定端口,计算机会随机分配端口,但是对于接收方而言必须要绑定端口否则发送方不知道给谁发送
2.1.2.3udp聊天器
-
vim编辑器下的使用快捷键:
- 跳转到指定的行:在命令行格式下先输入要切换到的行号,然后按G即可跳转到指定的行
- crtl+n:自动补全代码
- 使用vim编辑代码时,在要编辑的文本后写入:+行号即可直接定义到行号,例如:vim 01_test.py +10可直接定位到第十行
- d表示剪切,p表示粘贴
-
说明:在Ubuntu的另外一个网卡中就是本地环回,这里的IP地址可以实现同一台主机的不同程序之间进行通信和数据共享,且这个IP地址127.0.0.1是永远不会改变的
-
半双工,单工,全双工
- 单工:只能单向通信(例如收音机只能听而不能发)
- 半双工:可以双向通信,但同一时刻只能单向通信(例如对讲机同一时刻只能听或者讲)
- 全双工:同一时刻可以双向通信(例如手机在同一时刻既可以听又可以讲)
- socket套接字属于全双工,只是在使用的过程中没有体现全双工(例如微信一般使用是接收到对方的信息后才会发送对应的信息而不会同时在听信息的同时发送)
-
代码实现
-
主机1
import socket # 发送信息 def send_msg(udp_socket,dest_ip,dest_port): send_data = input("请输入要发送的信息:") udp_socket.sendto(send_data.encode("utf-8"),(dest_ip,dest_port)) # 接受信息 def recv_msg(udp_socket): # 接受对方发送的信息 recv_data = udp_socket.recvfrom(1024) # 当程序接受不到信息时会发生堵塞,就是卡住了 print("对方发送的信息为:%s:发送方的信息为:%s" % (recv_data[0].decode("utf-8"),str(recv_data[1]))) # 一般函数之间空两行 def main(): udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_socket.bind(("",8800)) print("-----聊天器-----") print("输入数字1可以发送数据") print("输入数字2可以接受数据") print("输入数字0可以退出系统") # 获取对方的ip和端口 dest_ip = input("请输入对方的ip:") dest_port = int(input("请输入对方的端口号:")) while True: op = input("请输入指令:") if op == "1": send_msg(udp_socket,dest_ip,dest_port) # 这里必须要有参数,否则函数中的udp_socket,dest_ip,dest_port无法识别 elif op == "2": recv_msg(udp_socket) elif op == "0": break else: print("指令有误请重新输入!") udp_socket.close() if __name__ == "__main__": main()
-
主机2
import socket # 发送信息 def send_msg(udp_socket,dest_ip,dest_port): send_data = input("请输入要发送的信息:") udp_socket.sendto(send_data.encode("utf-8"),(dest_ip,dest_port)) # 接受信息 def recv_msg(udp_socket): # 接受对方发送的信息 recv_data = udp_socket.recvfrom(1024) # 当程序接受不到信息时会发生堵塞,就是卡住了 print("对方发送的信息为:%s:发送方的信息为:%s" % (recv_data[0].decode("utf-8"),str(recv_data[1]))) # 一般函数之间空两行 def main(): udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_socket.bind(("",9900)) print("-----聊天器-----") print("输入数字1可以发送数据") print("输入数字2可以接受数据") print("输入数字0可以退出系统") # 获取对方的ip和端口 dest_ip = input("请输入对方的ip:") dest_port = int(input("请输入对方的端口号:")) while True: op = input("请输入指令:") if op == "1": send_msg(udp_socket,dest_ip,dest_port) # 这里必须要有参数,否则函数中的udp_socket,dest_ip,dest_port无法识别 elif op == "2": recv_msg(udp_socket) elif op == "0": break else: print("指令有误请重新输入!") udp_socket.close() if __name__ == "__main__": main()
-
代码说明:主机1和主机2的代码只有一个端口号的差异,由于这里是在同一台主机下利用环回地址进行通信,因此端口号不能重复,如果是两台主机则端口号可以重复
-
内存说明:在一台主机发送信息时,如果连续发送多条信息,这些信息时储存在另一台主机的内存中,当另一台主机接受时才会从内存中释放显示,而接受也是一条一条的接受;也就是说如果连续发送多条数据到另一台主机,当数据使内存占满时就会导致另一台主机崩溃
-
2.1.3tcp
2.1.3.1tcp介绍
- tcp是不同于udp的另外一种网络通信方式
- udp和tcp的区别
- 写信就相当于udp,udp发送的信息可能会丢,udp通信模型中,在通信开始之前,不需要建立相关的链接,udp较于简单;udp通信中接收方不会对发送方发送的数据进行应答,也就是说发送方不知道数据是否发送成功
- 打电话就相当于tcp,tcp通信比较稳定,tcp通信模型中,在通信开始之前必须建立相关的链接,tcp较于复杂;tcp通信中接收方会对发送方发送的数据进行应答来保证数据发送成功,发送不成功或者数据丢失的相关信息接收方都会如实告诉发送方,发送方会重复发送数据来保证数据发送成功且完整
- 日常生活中下载和上传的概念
- 例如使用迅雷下载一部几个G的电影,发送方不会将电影一次性发送过来而是将其分成若干份一份一份发送过来,发送方发送一份数据接受方所接受需要的时间就是下载速度的体现,而下载完成后接受方会给发送方发送一个信息告诉发送方下载完毕请发送下一个数据,这个向发送方发送信息所需的时间就是上传速度的体现
- tcp是一个可靠传输
- tcp采用发送应答机制:发送方发送的每一个数据都必须接受到接受方的应答才认为该数据发送成功
- 超时重传:当发送方发送一个数据而接受方在规定时间内没有应答则发送方认为接受方没有接收到信息会自动重新发送数据
- 错误校验:tcp用一个校验和函数来判断数据是否有错误,在发送和接受都需要计算校验和来判定发送的数据是否被接受方完整接受
- 流量控制和堵塞管理:流量控制用来避免发送方发送过快而接受方来不及完全收下
- 如今的现实生活中几乎所有的通信都用tcp传输,因为tcp更加的稳定,保证数据传输的可靠性
2.1.3.2tcp客户端
-
tcp严格区分客户端和服务端
-
vim编辑器使用技巧:
- 当使用vim编辑器新建一个文档时如果忘记写要编辑的文档名,在保存退出的时候输入:w 文档名就会将编辑的文档保存到指定的文件中,然后输入q退出即可
- 在命令行模式下输入O可以在当前光标所在的行的前面添加一行并将光标置于该行的行首,按下o可以在当前行的后面插入一行并将光标置于该行的行首
- 在命令行下选择Y表示复制
- 在命令行下u可以撤销上一步操作,ctrl+r恢复撤销的操作
-
在Ubuntu的命令行下新建一个python文件时如果命名出现类似于()的内容时会报错,如果一定要在文件名上加入()可以使用转义字符:\(client\),就可以输出()
-
注意事项:
- 在使用vim编辑代码时由于误操作出现E325报错同时产生一个.swap结尾的交换文件,每次编辑文件时都会出现关于这个交换文件相关的提示,解决方案就是删除这个交换文件,直接在当前目录下删除:rm ***.swap(一般这个交换文件保存在源文件的同一个字目录下用来保护源文件)
2.1.3.3tcp服务器
- tcp服务器通信的原理:
- 服务器买了一个套接字0用来接受客户端发送的信息(这个套接字0只负责接电话但不负责为其服务),客户端1向套接字0发送信息请求服务,服务器分配了一个新的套接字1来为其服务同时将客户端1的IP地址也一并告诉套接字1,然后断开客户端1和套接字0之间的通信,方便其它客户端进行访问
- 客户端2向套接字0发送请求信息,服务器又分配一个新的套接字2来服务客户端2同时将客户端2的IP地址一并告诉套接字2,然后断开客户端2与套接字0的通信,方便其它客户端访问
- 拆包:a, b = (123,134),当变量的个数和元祖内元素的个数相同时,会将元祖内的元素按顺序给变量,这里a=123,b=134
- 必须是服务器先运行生成可以接受客户端通信请求的套接字,客户端才能向服务器发送请求
2.1.3.4tcp客服服务系统
-
客户端代码编写:
import socket def main(): # 2.链接服务器 print("---tcp聊天器系统客户端---") print("输入字母y进入聊天系统,输入字母n推出聊天系统") is_ = input("请输入要执行的操作:") if is_ == "y": print("客户端以开启") while True: # 循环链接服务器 # 1.创建tcp的套接字 tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server_ip = input("请输入要链接的服务器ip:") server_port = int(input("请输入要链接的端口:")) tcp_socket.connect((server_ip,server_port)) # 此处会有堵塞,只有链接成功才会执行后续代码 print("连接服务器成功") print("输入数字1开启服务请求,输入数字0退出服务") # 此处用来实现链接的服务器不是想要的服务器时,可以即时退出重新链接其它服务器 server_temp = int(input("请输入服务数字:")) if server_temp == 1: # 输入服务数字为1时开启会话服务 print("服务会话开启,输入exit退出会话") while True: # 循环发送数据和接受数据 # 3.发送数据/接受数据 send_data = input("请输入要发送的数据:") tcp_socket.send(send_data.encode("utf-8")) # 注意:发送的数据不能为空 if send_data == "exit": # 发送exit意在向服务器说明要退出会话 break print("等待服务器回复……") recv_data = tcp_socket.recv(1024).decode("utf-8") # 接受服务器发送的数据 print("服务器回复为:%s" % recv_data) if recv_data == "exit": # 当服务器回复exit是意味着服务器要退出会话 break if server_temp == 0: # 当输入数字为0时关闭会话服务 print("会话服务断开,输入y继续连接下一个服务器,输入n关闭客户端") tcp_socket.send("exit".encode("utf-8")) # 向服务器发送一个exit来表明自己要退出会话 is__ = input("会话服务以断开,输入y继续连接下一个服务器,输入n关闭客户端,请输入要执行的操作:") if is__ == "n": break elif is__ == "y": tcp_socket.close() # 这里必须要关闭套接字才能重新连接服务器,否则如果连接同一个服务器会显示已经连接无法重新连接 continue if is_ == "n" or is__ == "n": # 4.关闭套接字 print("客户端已关闭") tcp_socket.close() if __name__ == "__main__": main()
-
服务器代码编写:
import socket
def main():
# 买个手机:创建套接字,以下用手机来帮助理解服务器的工作流程
tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 插入手机卡:绑定本地信息
tcp_server_socket.bind(("", 7890))
# 将手机设置为正常的响铃模式:让默认的套接字由主动变为被动,listen(也可以理解为原来的手机只能打电话不能接电话而现在就可以接电话,监听套接字只负责接电话不负责为其服务)
tcp_server_socket.listen(128)
print("---tcp聊天器系统服务器---")
while True: # 循环为多个客户端服务
print("输入y开启服务器,输入n关闭服务器")
is_ = input("请输入要执行的操作:")
if is_ == "n":
break
if is_ == "y":
print("服务器已开启")
print("客户端请求中……")
# 等待别人打电话到来:等待客户端连接,accept
new_client_server_socket,client_addr = tcp_server_socket.accept() # accept()的返回值是一个元祖,且元祖固定有两个元素,分别为服务套接字和客户端的IP地址及端口号,这里用解包来传值,该部分会堵塞,当有客户端发送连接请求是会释放堵塞(就类似于input从键盘输入数据,必须在键盘输入数据才能运行下一行程序)
print("客户端请求成功,客户端的IP地址为%s,端口号为%d" % (client_addr[0], client_addr[1]))
print("回复exit可以退出当前客户端服务")
while True: # 循环为一个客户端服务
# 接受客户端的请求
print("等待客户端输入请求……")
recv_data = new_client_server_socket.recv(1024).decode("utf-8") # 这里的recv_data是一个普通的数据而不是一个元祖
print("客户端输入为%s" % recv_data)
if recv_data == "exit":
break
# 会送一部分数据给客户端
server_send = input("服务器回复为:")
new_client_server_socket.send(server_send.encode("utf-8"))
if server_send == "exit":
break
# 关闭套接字
new_client_server_socket.close()
print("客户服务完毕,开始下一个客户端的服务")
print("服务器已关闭")
tcp_server_socket.close()
if __name__ == "__main__":
main()
2.1.3.5tcp下载文件
2.1.3.5.1文件下载器案例
-
客户端代码
from socket import * def main(): # 创建socket tcp_client_socket = socket(AF_INET, SOCK_STREAM) # 目的信息 server_ip = input("请输入服务器ip:") server_port = int(input("请输入服务器port:")) # 链接服务器 tcp_client_socket.connect((server_ip, server_port)) # 输入需要下载的文件名 file_name = input("请输入要下载的文件名:") # 发送文件下载请求 tcp_client_socket.send(file_name.encode("utf-8")) # 接受对方发送过来的数据,最大接受字节1k recv_data = tcp_client_socket.recv(1024 * 1024) # 就是1M大小 # 如果接受到文件就创建文件否则不创建 if recv_data: # with如果打开文件错误会报错,但是如果打开文件成功但文件操作错误不会报错会自动关闭打开的文件 with open(file_name, "wb") as f: # 这种书写方式在文件执行完毕后会自动关闭文件(无论文件操作是否出错),"[接受]"+file_name是新建的文件名,wb的b表示二进制,因为recv_data接受的数据是二进制,w表示以只写方式打开 f.write(recv_data) # 等价于: # f = open("文件名", "wb") # try: # f.write(recv_data) # except: # f.close() # 关闭套接字 tcp_client_socket.close()
-
服务器代码
import socket def send_file_2_client(new_client_socket, client_addr): # 接受客户端要下载的文件名 file_name = new_client_socket.recv(1024).decode("utf-8") print("客户端【%s】要下载的文件是【%s】" % (str(client_addr), file_name)) # 定义一个空的内容,用来存储需要下载的文件的内容 file_content = None try: f = open(file_name, "rb") file_content = f.read() f.close except Exception as ret: print("没有要下载的文件") # 如果有内容就将内容发送给客户端 if file_content: new_client_socket.send(file_content) def main(): tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_server_socket.bind(("", 7890)) tcp_server_socket.listen(128) # 128可以简单的理解为可以同时被128台客户端连接,但又不能简单的这么理解,实际上和不同的操作系统有关 while True: # 可以重复进行服务 new_client_socket, client_addr = tcp_server_socket.accept() send_file_2_client(new_client_socket, client_addr) new_client_socket.close() tcp_server_socket.close() if __name__ == '__main__': main()
2.1.3.5.2tcp注意点
- 对于QQ而言,两个QQ之间是通过腾讯的服务器进行通信的,也就是说,用户QQ都是客户端,只有腾讯是服务器端,用户端的QQ不需要绑定端口(客户端向服务器发送数据请求的时候腾讯服务器可以知道客户端的端口号);用户端1和用户端2同时连接腾讯服务器,用户端1将需要发送的信息发送给腾讯服务器,腾讯服务器接受到服务器1到信息后将其转发给客户端2(腾讯服务器起到转发信息的功能),因此客户端之间的通信都是通过腾讯的服务器来进行的(腾讯服务器会保留用户的通信信息很长时间)
- 对于tcp通信而言,客户端一般不需要绑定端口(例如需要在一台主机上多开QQ时,如果都绑定端口,这时就可能出现一个其它软件的端口占用了QQ的端口导致无法通信);而对于udp通信而言,双方通信一般都需要绑定端口
- tcp服务器可以通过listen将主动连接变为被动连接,也就是说服务器必须要使用listen才可以被客户端连接
- tcp通信客户端连接服务器时需要使用connect进行连接,只有连接成功才可以发送数据,而udp可以不连接直接发送;tcp是面向连接的通信,udp是面向无连接的通信
- 关闭listen所在的套接字意味着服务器不能在被客户端连接,但原先以连接的通信是正常的;关闭accept返回的套接字意味着当前所服务的客户端已经服务完毕了
- 解除服务器端recv堵塞的两种方式:一种是客户端发送信息,另一种是客户端关闭连接套接字(服务完毕),通过返回recv的长度来判断用户是否已经下线