一, IP 地址
IP 地址用来标记网络上的一台电脑, 在本地局域网上是唯一的;
Linux 中用 ifconfig 命令来查看 IP 地址, Windows 中用 ipconfig 命令;
禁用网卡: sudo ifconfig ens33 down
开启网卡: sudo ifconfig ens33 up
IP 地址的分类: IPV4,IPV6
每个 IP 地址由两个部分组成: 网络地址, 主机地址
A 类 IP 地址
一个 A 类 IP 地址由 1 字节的网络地址和 3 字节主机地址组成, 网络地址的最高位必须是 "0",
地址范围 1.0.0.1-126.255.255.254
二进制表示为: 00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
可用的 A 类网络有 126 个, 每个网络能容纳 1677214 个主机
B 类 IP 地址
一个 B 类 IP 地址由 2 个字节的网络地址和 2 个字节的主机地址组成, 网络地址的最高位必须是 "10",
地址范围 128.1.0.1-191.255.255.254
二进制表示为: 10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
可用的 B 类网络有 16384 个, 每个网络能容纳 65534 主机
C 类 IP 地址
一个 C 类 IP 地址由 3 字节的网络地址和 1 字节的主机地址组成, 网络地址的最高位必须是 "110"
范围 192.0.1.1-223.255.255.254
二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110
C 类网络可达 2097152 个, 每个网络能容纳 254 个主机
D 类地址用于多点广播
D 类 IP 地址第一个字节以 "1110" 开始, 它是一个专门保留的地址.
它并不指向特定的网络, 目前这一类地址被用在多点广播 (Multicast) 中
多点广播地址用来一次寻址一组计算机 s 地址范围 224.0.0.1-239.255.255.254
E 类 IP 地址
以 "1111" 开始, 为将来使用保留
E 类地址保留, 仅作实验和开发用
私有 ip
在这么多网络 IP 中, 国际规定有一部分 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
注意
IP 地址 127.0.0.1~127.255.255.255 用于回路测试,
如: 127.0.0.1 可以代表本机 IP 地址, 用 http://127.0.0.1 就可以测试本机中配置的 web 服务器.
二, 端口
标识主机中的应用进程.
端口是通过端口号来标记的, 端口号只有整数, 范围是从 0 到 65535
注意: 端口数不一样的 * nix 系统不一样, 还可以手动修改
知名端口(Well Known Ports)
知名端口是众所周知的端口号, 范围从 0 到 1023
80 端口分配给 HTTP 服务
21 端口分配给 FTP 服务
一般情况下, 如果一个程序需要使用知名端口的需要有 root 权限
动态端口(Dynamic Ports)
动态端口的范围是从 1024 到 65535
之所以称为动态端口, 是因为它一般不固定分配某种服务, 而是动态分配.
动态分配是指当一个系统程序或应用程序程序需要网络通信时, 它向主机申请一个端口, 主机从可用的端口号中分配一个供它使用.
当这个程序关闭时, 同时也就释放了所占用的端口号.
三, socket
socket(简称 套接字) 是进程间通信的一种方式, 它与其他进程间通信的一个主要不同是:
它能实现不同主机间的进程间通信, 我们网络上各种各样的服务大多都是基于 Socket 来完成通信的
例如我们每天浏览网页, QQ 聊天, 收发 email 等等
创建 socket
import socket
socket.socket(AddressFamily, Type) 返回值是一个套接字对象
- import socket
- # 创建 udp 的套接字
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- # ... 这里是使用套接字的功能(省略)...
- # 不用的时候, 关闭套接字
- s.close()
- import socket
- # 创建 tcp 的套接字
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # ... 这里是使用套接字的功能(省略)...
- # 不用的时候, 关闭套接字
- s.close()
函数 socket.socket 创建一个 socket, 该函数带有两个参数:
Address Family: 可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信), 实际工作中常用 AF_INET
Type: 套接字类型, 可以是 SOCK_STREAM(流式套接字, 主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字, 主要用于 UDP 协议)
四, UDP 网络程序
User Datagram Protocol 的简称, 中文名是用户数据报协议
主机使用网络调试助手接收消息, 虚拟机网络需要设置为桥接模式(IP 在一个网段)
我遇到的问题:
发消息: NAT 模式也可以发消息, 但因为主机防火墙没有关, 虚拟机 ping 不通主机
收消息: 改完桥接就断网, 主机网络设置中两个 VM 的禁用就可以顺利改为桥接
- def main():
- # 创建一个 udp 套接字
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- # 发送数据到指定电脑的指定程序中
- # b 表示以字节 bytes 类型发送, 否则会报错
- udp_socket.sendto(b"hahahah000000", ("192.168.0.105", 8080))
- # 关闭套接字
- udp_socket.close()
- if __name__ == "__main__":
- main()
编码转换:
str->bytes:encode 编码
bytes->str:decode 解码
- import socket
- def main():
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- data = input("需要发送的数据:")
- udp_socket.sendto(data.encode("utf-8"), ("192.168.0.105", 8080))
- udp_socket.close()
- if __name__ == "__main__":
- main()
循环发送数据:
- import socket
- def main():
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- while True:
- data = input("请输入要传输的数据:")
- if data == "exit":
- break
- udp_socket.sendto(data.encode("utf-8"), ("192.168.0.105", 8080))
- udp_socket.close()
- if __name__ == '__main__':
- main()
以上测试我主机的 ip 为 192.168.0.105:8080, 虚拟机 IP 为 192.168.0.115:54321(NAT 模式时为 192.168.152.128 可以发送不能接收)
接收 udp 数据:
- from socket import *
- # 创建套接字
- udp_socket = socket(AF_INET, SOCK_DGRAM)
- # 绑定本地的相关信息, 如果一个网络程序不绑定, 系统会随机分配
- local_addr = ("", 54321) # ip 一般不用写, 表示本机的任何一个 ip
- udp_socket.bind(local_addr)
- # 接收数据
- recv_data = udp_socket.recvfrom(1024) # 1024 表示本次接收的最大字节数
- # recv_data 中存储的是一个元组(接收的数据,(发送方的 ip, 发送方的端口))
- # 接收中文需要解码: utf-8 不能解码 bytes, 因为 Windows 默认编码是 gbk
- print("%s : %s" % (recv_data[1], recv_data[0].decode("gbk")))
- udp_socket.close()
发送数据的流程:
(若没有绑定端口, 每一次系统随机分配的端口号不一样)
创建套接字
发送数据
关闭
接收数据的流程:
创建套接字
绑定 IP 和端口
接收数据
关闭
五, UDP 聊天器
单工: 单向, 如收音机
半双工: 双向, 同一时刻只能有一个方向, 如对讲机
全双工: 双向, 同一时刻可以两方传输, 如打电话
socket 套接字是全双工;
- import socket
- def main():
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- udp_socket.bind(('', 8765))
- while True:
- print("1. 发送消息 2. 接收消息 0. 退出系统")
- op = input("请输入功能")
- if op == "1":
- send_mess = input("要发送的数据:")
- udp_socket.sendto(send_mess.encode('utf-8'), ('192.168.0.105', 8080))
- elif op == "2":
- recv_mess = udp_socket.recvfrom(1024)
- print("接收到的数据:%s" % recv_mess[0].decode('gbk'))
- elif op == "0":
- break
- else:
- print("输入有误")
- udp_socket.close()
- if __name__ == "__main__":
- main()
六, TCP 协议
TCP 协议, 传输控制协议 (英语: Transmission Control Protocol, 缩写为 TCP) 是一种面向连接的, 可靠的, 基于字节流的传输层通信协议, 由 IETF 的 RFC 793 定义.
TCP 通信需要经过创建连接, 数据传送, 终止连接三个步骤.
TCP 通信模型中, 在通信开始之前, 一定要先建立相关的链接, 才能发送数据, 类似于生活中,"打电话""
1. 面向连接
通信双方必须先建立连接才能进行数据的传输, 双方都必须为该连接分配必要的系统内核资源, 以管理连接的状态和连接上的传输.
双方间的数据传输都可以通过这一个连接进行.
完成数据交换后, 双方必须断开此连接, 以释放系统资源.
这种连接是一对一的, 因此 TCP 不适用于广播的应用程序, 基于广播的应用程序请使用 UDP 协议.
2. 可靠传输
1)TCP 采用发送应答机制
TCP 发送的每个报文段都必须得到接收方的应答才认为这个 TCP 报文段传输成功
2)超时重传
发送端发出一个报文段之后就启动定时器, 如果在定时时间内没有收到应答就重新发送这个报文段.
TCP 为了保证不发生丢包, 就给每个包一个序号, 同时序号也保证了传送到接收端实体的包的按序接收. 然后接收端实体对已成功收到的包发回一个相应的确认 (ACK); 如果发送端实体在合理的往返时延(RTT) 内未收到确认, 那么对应的数据包就被假设为已丢失将会被进行重传.
3)错误校验
TCP 用一个校验和函数来检验数据是否有错误; 在发送和接收时都要计算校验和.
4) 流量控制和阻塞管理
流量控制用来避免主机发送得过快而使接收方来不及完全收下.
udp 与 tcp 区别:
udp 通信模型中, 在通信开始之前, 不需要建立相关的链接, 只需要发送数据即可, 类似于生活中,"写信""
udp 通信模型中, 在通信开始之前, 一定要先建立相关的链接, 才能发送数据, 类似于生活中,"打电话""
七, TCP 客户端
tcp 的客户端要比服务器端简单很多, 如果说服务器端是需要自己买手机, 查手机卡, 设置铃声, 等待别人打电话流程的话, 那么客户端就只需要找一个电话亭, 拿起电话拨打即可, 流程要少很多
- import socket
- def main():
- # 创建套接字
- tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 建立连接
- tcp_client.connect(('192.168.0.105', 8080))
- send_mess = input("要发送的数据:")
- # 发送消息
- tcp_client.send(send_mess.encode('utf-8'))
- # 接收消息
- recv_mess = tcp_client.recv(1024)
- print(recv_mess.decode('gbk'))
- # 关闭套接字
- tcp_client.close()
- if __name__ == "__main__":
- main()
八, TCP 服务器
在程序中, 如果想要完成一个 tcp 服务器的功能, 需要的流程如下:
socket 创建一个套接字
bind 绑定 ip 和 port
listen 使套接字变为可以被动链接
accept 等待客户端的链接
recv/send 接收发送数据
- import socket
- def main():
- # 创建套接字(买手机
- tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 绑定 ip 和端口(插手机卡
- tcp_server.bind(('', 7788))
- # 使套接字变为被动套接字, 套接字默认是主动的(设为正常接听状态
- tcp_server.listen(128)
- # 等待客户端的链接, 返回值是元组(等待电话打入
- # client_socket 用来为这个客户端服务, tcp_server 继续等待新客户端的连接
- client_socket, client_addr = tcp_server.accept()
- print(client_addr)
- # 接收消息
- recv_data = client_socket.recv(1024)
- print(recv_data.decode('gbk'))
- # 发送消息
- client_socket.send("hahahaha...".encode('utf-8'))
- # 关闭套接字
- client_socket.close()
- tcp_server.close()
- if __name__ == "__main__":
- main()
循环为多个客户端服务:
- import socket
- def main():
- tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- tcp_server.bind(('', 9876))
- tcp_server.listen(128)
- while True:
- print("等待一个新的客户端到来...")
- client_socket, client_addr = tcp_server.accept()
- recv_data = client_socket.recv(1024)
- print("来自 %s : %s" % (client_addr, recv_data.decode('gbk')))
- client_socket.close()
- print("服务完毕.")
- tcp_server.close()
- if __name__ == "__main__":
- main()
循环为多个客户端服务并多次服务一个客户端:
- import socket
- def main():
- tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- tcp_server.bind(('', 7788))
- tcp_server.listen(128)
- while True:
- print("等待一个新的客户端到来...")
- client_socket, client_addr = tcp_server.accept()
- print("客户端 %s:%s 连接成功" % client_addr)
- while True:
- recv_data = client_socket.recv(1024)
- # 如果 recv 解堵塞, 有两种可能:
- # 客户端发过来数据
- # 客户端调用了 close(recv_data 为空)
- if recv_data:
- print("来自 %s : %s" % (client_addr, recv_data.decode('gbk')))
- else:
- break
- client_socket.close()
- print("------- 服务完毕 -------")
- tcp_server.close()
- if __name__ == "__main__":
- main()
listen 中的值代表多少个客户端可以同时链接
tcp 注意点:
tcp 服务器一般情况下都需要绑定, 否则客户端找不到这个服务器
tcp 客户端一般不绑定, 因为是主动链接服务器, 所以只要确定好服务器的 ip,port 等信息就好, 本地客户端可以随机
tcp 服务器中通过 listen 可以将 socket 创建出来的主动套接字变为被动的, 这是做 tcp 服务器时必须要做的
当客户端需要链接服务器时, 就需要使用 connect 进行链接, udp 是不需要链接的而是直接发送, 但是 tcp 必须先链接, 只有链接成功才能通信
当一个 tcp 客户端连接服务器时, 服务器端会有 1 个新的套接字, 这个套接字用来标记这个客户端, 单独为这个客户端服务
listen 后的套接字是被动套接字, 用来接收新的客户端的链接请求的, 而 accept 返回的新套接字是标记这个新客户端的
关闭 listen 后的套接字意味着被动套接字关闭了, 会导致新的客户端不能够链接服务器, 但是之前已经链接成功的客户端正常通信.
关闭 accept 返回的套接字意味着这个客户端已经服务完毕
当客户端的套接字调用 close 后, 服务器端会 recv 解堵塞, 并且返回的长度为 0, 因此服务器可以通过返回数据的长度来区别客户端是否已经下线
九, 文件下载器
with as 操作文件
- # 不用 close
- with open("文件名", "wb") as f:
- f.write("111222333".encode('gbk'))
- # 相当于这种写法, 即使出现异常, 也会 close
- f = open("文件名", "w")
- try:
- f.write("aabbcc")
- except:
- f.close()
文件下载客户端
- import socket
- def main():
- # 创建套接字
- tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 链接服务器
- tcp_client.connect(("192.168.0.115", 9876))
- # 发送下载的文件名
- file_name = input("输入要下载的文件名:")
- tcp_client.send(file_name.encode('utf-8'))
- # 接收文件数据
- recv_data = tcp_client.recv(1024)
- # 将接收到的数据包存到文件
- if recv_data:
- with open("[接收]"+file_name, "wb") as f:
- f.write(recv_data)
- # 关闭套接字
- tcp_client.close()
- if __name__ == "__main__":
- main()
文件下载服务器
- import socket
- def send_file(client_socket, client_addr):
- # 接收客户端需要下载的文件名
- file_name = client_socket.recv(1024)
- print("客户端 %s 需要下载的文件是:%s" % (client_addr, file_name.decode('utf-8')))
- file_content = None
- # 打开文件 读取数据
- # with 的前提是打得开的前提下, 处理读写异常, 这里不能用 with
- try:
- f = open(file_name, 'rb')
- file_content = f.read()
- f.close()
- except Exception as ret:
- print("没有要下载的文件 %s:%s" % (file_name, ret))
- # 发送文件数据给客户端
- if file_content:
- client_socket.send(file_content)
- print("文件发送成功")
- def main():
- # 创建套接字
- tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 绑定 ip 和端口
- tcp_server.bind(('', 9876))
- # 开启监听
- tcp_server.listen(128)
- # 接收客户端连接
- client_socket, client_addr = tcp_server.accept()
- # 发送文件
- send_file(client_socket, client_addr)
- # 关闭套接字
- client_socket.close()
- tcp_server.close()
- if __name__ == "__main__":
- main()
来源: http://www.bubuko.com/infodetail-3115592.html