网络编程基础
网络
- 使用网络能够把多方链接在一起,然后可以进行数据传递
- 所谓的就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信
ip地址
目的:用来标记网络上的一台电脑(不允许重复)
-
如何查看当前电脑的ip:
- Linux:ifconfig,中的inet项
- Windows:ipconfig
-
ip地址的分类
ipv4:v4就是第四个版本,由四部分组成,eg:192.168.14.91,每组数里面的最大值为255,最小值为0。通常,前三部分为网络号,最后一位为主机号,前三部分均相同即表示电脑在同一个网络下,主机号的0和255不能使用(是用于回路测试的)。
分类:
如果用仅用第一部分作为网络号,后三部分为主机号,则为A类地址;
如果使用前两部分作为网络号,后两部分为主机号,则为B类地址;
前三部分作为网络网,后一部分作为主机号则为C类地址【最常用】;
D类地址用于多点广播,第一字节以1110开始,是一个专门的保留地址,它并不指向特定的网络,多点广播地址用来一次寻址一组计算机s地址范围为224.0.0.1-239.255.255.254;
E类ip地址以1111开始,为将来使用保留,进作实验和开发用。
私有ip:在这么多网络中,国际规定有一部分ip地址是用于我们的局域网使用,也就是属于私网ip。他们的范围是10.*和172.*以及192.*ipv6:第六个版本,ip1235均为实验版本
端口
端口号也是一个数字,用来对电脑上的进程进行标识(从0-65536(2的16次方)的整数)
-
分类
- 知名端口:众所周知的端口(0-1023),eg:80分配给HTTP服务,21分配给FTP服务,即大于1024的端口随便使用
- 动态端口:从1024-65535,之所以称之为动态端口,是因为它一般不固定分配,而是动态分配(当一个系统程序或应用程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用,当程序关闭时,同时也就释放了所占用的端口号)
socket简介
socket:套接字
socket是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:
它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于socke来完成通信的,例如QQ,收发email
在不同的语言中,socket的流程是一样的
创建一个socket(tcp套接字)
import socket
skt = socket.socket(socket.AF_INEF, socket.SOCK_STREAM)#创建tcp套接字,因为tcp协议是流式传输,所以第二个参数为SOCK_STREAM
#第一个参数为协议族,AF_INEF表示使用的是ipv4的协议族
#socket.socket()是一个类,此语句创建了一个套接字对象
#这里是使用套接字的功能(省略)
skt.close() #不用的时候关闭套接字
创建一个udp socket(udp套接字)
import socket
udp_skt = socket.socket(socket.AF_INEF, SOCK_DGRAM) #因为udp协议传输的为报文,所以第二个参数为SOCK_DGRAM
#使用套接字的功能(省略)
udp_skt.close() #不用的时候,关闭
UDP
UDP(User Datagram Protocol)通信模型中,不需要建立相关的链接,只需要发送数据即可,类似于生活中“写信”——不安全
udp发送数据demo
from socket import *
#1.创建套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)
#2.准备接受方地址
#'192.168.205.11'表示目的ip
#8080表示目的端口
dest_ip = "192.168.205.1"
dest_port = 8080
dest_addr = (dest_ip, dest_port) #对方的ip和端口用元组
#3.从键盘获得数据
send_data = input(">")
#4.发送数据到指定电脑上的指定程序中
udp_socket.sendto(send_data.encode("utf-8"), dest_addr) #两个参数:要发送的内容,接受方地址
#关闭套接字
udp_socket.close()
可配合网络调试助手接收到以上程序发送的消息
udp接收数据demo
如果程序要接收数据,必须要一个固定的端口:使用bind绑定
客户端:
from socket import *
HOST = "192.168.0.113" #因为是自己写程序在本机上验证,所以这里的ip就是本机ip
PORT = 8888
BUFSIZE = 1024
ADDR = (HOST, PORT)
udpCliSock = socket(AF_INET, SOCK_DGRAM) #第一个参数表示网络编程,第二个参数表示报文传输
#udpCliSock.bind(("",7788)) #可以使用此句给自己绑定端口,这样就不会使用系统随机分配的端口
while True:
data = input(">")
if not data:
break #如果没有输入数据,跳出循环,结束程序
udpCliSock.sendto(data.encode("utf-8"),ADDR) #将数据发送给目的地,
# 因为在发送数据之前没有给自己绑定端口号,所以系统会随机分配一个端口号
data, ADDR = udpCliSock.recvfrom(BUFSIZE) #BUFSIZE表示本次接收的最大字节数
#接收从服务器端返回的数据,recvfrom在数据没有到来的时候会阻塞
if not data:
break
print(data.decode("utf-8"))
udpCliSock.close()
服务器端:
from socket import *
import time
HOST = "" #本机ip,因为是做服务器,所以可以不用写,表示本机的任意一个ip
PORT = 8888 #端口号,不是小于1024的都可以
BUFSIZ = 1024 #缓冲区大小
ADDR = (HOST, PORT)
udpServSock = socket(AF_INET, SOCK_DGRAM) #获得一个socket
udpServSock.bind(ADDR) #绑定本堤的相关信息(ip与端口),如果一个网络程序不绑定,则系统会随机分配
while True:
print("waitting for message...")
data, addr = udpServSock.recvfrom(BUFSIZ) #bufsize表示本次接收的最大字节数,recvfrom的返回值为数据和发送端的地址(ip与端口组成的元组)
#程序会在这里一直等待,直到接收到数据
data = data.decode("utf-8")
content = '[%s] %s' %(bytes(time.ctime(),"utf-8"), data)
print("收到数据:", data)
udpServSock.sendto(content.encode("utf-8"), addr) #把收到的数据发送回客户端
print("received from and return to:", addr)
udpServSock.close()
注意:严格来说,UDP并没有服务器和客户端的概念,想要收就recfrom,想要发就sendto
ps:代码不是从上到下写的,而是用到什么导入什么,用到什么定义什么
注意:同一个端口在同一时刻只能被一个程序所使用
- 单工:要么只能收数据,要么只能发数据
- 半双工:可以发送也可以接收数据,但是同一时刻只能收或者发
- 全双工:同一时刻既可以发也可以收
TCP(更常用)
简介:
- TCP协议,传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议
- TCP通信需要经过创建连接、数据传送、终止连接三个步骤
- TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中“打电话”
TCP和UDP的不同点:
- 面向连接
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞/流量控制
TCP注意点
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
- tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip,port等信息就好,本地客户端可以随机
- tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
- 当客户端需要连接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会recv接堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
ps:tcp有三次握手和四次挥手 (由操作系统底层自己完成)
客户端:
from socket import *
def creat_link():
"""
建立链接
:return: 套接字
"""
# 1. 创建tcp的套接字,ps:客户端一般不绑定端口号,但是可以绑定
tcp_socket = socket(AF_INET, SOCK_STREAM)
#tcp_socket.setsockopt(socket.SOL__SOCKET, socket.SO_REUSERADDR, 1) # 让服务器端可以先close,而不会出现地址正在使用中的问题(具体涉及到四次挥手)
# 2. 链接服务器
server_ip = input("server ip:")
server_port = None
try:
server_port = int(input("server port:"))
except:
print("Error! The port must be integer!")
exit()
server_addr = (server_ip, server_port)
tcp_socket.connect(server_addr)
print("connect has created")
return tcp_socket
def send_data(tcp_socket):
# 3. 发送数据/接收数据
send_data = input(">")
tcp_socket.send(send_data.encode("utf-8")) # udp用sendto方法(因为没有链接,所以每次发送都必须知道目的地),
# tcp因为有链接,所以使用send方法即可
if send_data == "exit()":
exit()
def recv_data(tcp_socket):
recv_msg = tcp_socket.recv(1024)
print("received msg from server: {}".format(recv_msg.decode("utf-8")))
def main():
tcp_socket = creat_link()
while True:
send_data(tcp_socket)
recv_data(tcp_socket)
#4. 关闭tcp套接字
tcp_socket.close()
if __name__ == "__main__":
main()
服务器端:
tcp服务器的流程如下(以买手机类比):
- socket创建一个套接字(买了手机)
- bind绑定ip和port(插上手机卡)
- listen使套接字变为可以被动链接(设置手机为正常接听状态——即能够响铃)
- accept等待客户端的链接(等待别人拨打)
- recv/send接收或发送数据(进行通话)
#服务器端必须绑定
from socket import *
def creat_server():
# 1. 买个手机(创建套接字)
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 2. 插入手机卡(绑定本地信息 bind)
server_port = None
try:
server_port = int(input("set server port:"))
except:
print("port must be integer!")
exit()
tcp_server_socket.bind(("", server_port))
# 3. 将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
print("waiting connect...")
tcp_server_socket.listen(128)
return tcp_server_socket
def creat_link(tcp_server_socket):
# 4. 等待别人的电话到来(等待客户端的链接 accept)
new_client_socket, client_addr = tcp_server_socket.accept() # accept的返回值是一个元组(client_socket, client_addr)
print(str(client_addr) + " has connected")
# 监听套接字默认会阻塞,直到有返回值
# a, b = (11, 22)类似这样的形式被称之为拆包,前后数量必须对应
# 通常是由一个套接字见监听,然后每accept到一个链接请求,就创建一个新的套接字与其链接(为客户端服务)
# 通常是服务器先接收,客户端先发送
return (new_client_socket, client_addr)
def recv_and_return_msg(new_client_socket, client_addr):
recv_data = new_client_socket.recv(1024) #如果客户端已经close(),则recv也会解阻塞,此时recv_data为空,
#如果是因为客户端发送了数据导致recv解阻塞,则recv_data不为空
#ps:客户端无法发送空内容
if recv_data.decode("utf-8") == "exit()":
return "exit()"
print(str(client_addr) + ":", end="")
print(recv_data.decode("utf-8"))
new_client_socket.send(recv_data)
def close_all_socket(tcp_server_socket, new_client_socket):
# 关闭套接字
tcp_server_socket.close()
new_client_socket.close()
print("Link has be closed")
def main():
tcp_server_socket = creat_server()
new_client_socket, client_addr = creat_link(tcp_server_socket)
while True:
tag = recv_and_return_msg(new_client_socket,client_addr)
if tag == "exit()":
close_all_socket(tcp_server_socket, new_client_socket)
exit()
if __name__ == "__main__":
main()
案例:文件下载器
客户端:
from socket import *
def main():
#1. 创建套接字
tcp_socket = socket(AF_INET, SOCK_STREAM)
#2. 获取服务器的ip, port
dest_ip = input("server ip:")
dest_port = int(input("server port:"))
#3. 链接服务器
tcp_socket.connect((dest_ip, dest_port))
#4. 获取下载文件的名字
file_name = input("请输入要下载的文件名字:")
#5. 将文件名字发送给服务器
tcp_socket.send(file_name.encode("utf-8"))
#6. 接收文件中的数据
recv_data = tcp_socket.recv(1024*1024) #为了使程序简化,假设文件最大1M
#7. 保存接收到的数据到文件中
with open("[副件-]" + file_name, "wb") as f:
f.write(recv_data)
print("文件:[{}]下载完成!".format(file_name))
#8. 关闭套接字
tcp_socket.close()
if __name__ == "__main__":
main()
服务器端:
from socket import *
def creat_server():
# 1. 买个手机(创建套接字)
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 2. 插入手机卡(绑定本地信息 bind)
server_port = None
try:
server_port = int(input("set server port:"))
except:
print("port must be integer!")
exit()
tcp_server_socket.bind(("", server_port))
# 3. 将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
print("waiting connect...")
tcp_server_socket.listen(128) #listen的参数一般写128,和同一时刻服务器端建立的连接数有关系,但是主要由操作系统决定,并不是说写128就是最多128个连接
return tcp_server_socket
def creat_link(tcp_server_socket):
# 4. 等待别人的电话到来(等待客户端的链接 accept)
new_client_socket, client_addr = tcp_server_socket.accept() # accept的返回值是一个元组(client_socket, client_addr)
print(str(client_addr) + " has connected")
# 监听套接字默认会阻塞,直到有返回值
# a, b = (11, 22)类似这样的形式被称之为拆包,前后数量必须对应
# 通常是由一个套接字见监听,然后每accept到一个链接请求,就创建一个新的套接字与其链接(为客户端服务)
# 通常是服务器先接收,客户端先发送
return (new_client_socket, client_addr)
def recv_and_return_msg(new_client_socket, client_addr):
file_name = new_client_socket.recv(1024)
if file_name.decode("utf-8") == "exit()":
return "exit()"
print(str(client_addr) + ":", end="")
print("正在发送文件:" + file_name.decode("utf-8"))
with open(file_name,"r") as f:
content = f.read()
new_client_socket.send(content.encode("utf-8"))
def close_all_socket(tcp_server_socket, new_client_socket):
# 关闭套接字
tcp_server_socket.close()
new_client_socket.close()
print("Link has be closed")
def main():
tcp_server_socket = creat_server()
new_client_socket, client_addr = creat_link(tcp_server_socket)
recv_and_return_msg(new_client_socket,client_addr)
close_all_socket(tcp_server_socket, new_client_socket)
if __name__ == "__main__":
main()
结合多线程实现群聊
客户端:
from socket import *
import time
import logging as logger
import concurrent.futures as futures
logger.basicConfig(level=logger.DEBUG)
class Tcp_Client:
def __init__(self, host = "127.0.0.1", port = 9999):
self.sock = socket(AF_INET, SOCK_STREAM)
self.sock.connect((host, port))
def send(self, msg):
"""
向服务器发送信息
:param msg:
:return:
"""
self.sock.send(msg.encode("utf-8"))
def read(self):
"""
:return:
"""
while True:
data = self.sock.recv(1024)
print(data.decode("utf-8"))
def main():
ex = futures.ThreadPoolExecutor(max_workers = 5)
tc = Tcp_Client()
ex.submit(tc.read)
while True:
data = input(">")
tc.send(data)
main()
服务器端:
from socket import *
import logging as logger
import concurrent.futures as futures
logger.basicConfig(level=logger.DEBUG)
class TcpServer:
def __init__(self, port = 9999):
self.host = "" #由于是服务器端,所以host为空
self.port = port
self.clients = []
self.ex = futures.ThreadPoolExecutor(max_workers = 5)
def run(self):
logger.info("服务器启动,监听端口:%d" % self.port)
self.server_sock = socket(AF_INET, SOCK_STREAM)
self.server_sock.bind((self.host, self.port))
self.server_sock.listen(5)
while True:
client_socket, client_addr = self.server_sock.accept()
self.ex.submit(self.read, client_socket)
logger.info("客户端{}:{}建立连接".format(client_addr[0],str(client_addr[1])))
self.clients.append((client_socket, client_addr))
def read(self, client_socket):
while True:
data = client_socket.recv(1024)
print(data.decode("utf-8"))
for client in self.clients:
sock = client[0]
addr = client[1]
if sock != client_socket:
info = addr[0] + ":" + str(addr[1]) + ":" + data.decode("utf-8")
logger.info("向客户端{}:{}发送信息".format(addr[0], str(addr[1])))
sock.send(info.encode("utf-8"))
TcpServer().run()
TCP与UDP的流程对比
udp: tcp客户端: tcp服务器端:
socket socket socket
bind connect bind
recvfrom/sendto send/recv listen
close close accept
recv/send
close