宿舍 2019-12-09
这是我的第二篇博客,很遗憾第一篇博客没有得到应有的认可。
可能是因为原理介绍和实操部分不够多,只是单纯分析了某一条指令在打开网页过程中,输出的变化。
在我的第二篇博客中把相关原理介绍的更加详细了,同时丰富了程序代码部分的介绍。
本文对通信相关知识点(如socket套接字、TCP/IP、HTTP通信协议)、hello/hi网络聊天程序代码、python socke接口与Linux socket api之间的关系三个方面做了相关介绍
一、网络通信相关知识
首先必须明确一点,我们进行网络通信之前,必须知道五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
1.Socket套接字
在介绍套接字之前,需要明确一点,套接字并不是某种网络协议,它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息。
出现背景:应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。
作用:应用层可以和传输层通过socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。
套接字之间的连接过程分为三个步骤:服务器监听、客户端请求、连接确认。需要进过三次握手。
Socket基本通信模型:
三次握手流程图:
2.TCP/IP协议
在介绍该协议之前需要理清一个概念:TCP/IP并不是单纯的指TCP协议,它是一系列网络协议的总和,是Internet的核心协议。
基于TCP/IP的参考模型将协议分为四个层次:链路层、网络层、传输层、应用层。
TCP/IP协议族层层封装,最上面的是应用层,里面有HTTP、FTP等协议;第二层是传输层,我们熟知的TCP/UDP协议就位于该层中;、
第三层是网络层,里面的IP协议负责对数据加上IP地址和其他的数。据以确定传输的目标;
第四层是数据链路层,这个层次为待传送的协议加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备
下图为一个完整的数据封装过程:
3.TCP连接
它是传输层的一种传输控制协议,与之对应的有UDP连接。
TCP、UDP的区别:
(1).TCP面向连接的场景,通信前双方必须先建立连接;而UDP是无连接的通信,直接将数据发送给对应主机。
(2).TCP有滑动窗口、差错控制等来提供可靠的服务,传输的数据无差错、不丢失、不重复、按序到达;UDP不保证可靠交付。
(3).TCP占用系统资源多、实时性差;UDP反之。
真实场景举例:
我们使用的qq、微信发送消息时,采用的都是UDP连接。我们发送消息时对方并不知道你要发送消息给他。
使用qq发送文件或压缩包数据时,会有两种选项,离线发送对应的即是UDP连接;但在线发送需要对方确认接收后才能将数据包发送过去,此为TCP连接。
通常情况下,Socket连接就是TCP连接,本实验也是基于TCP连接做的网络通信。
4.HTTP协议
HTTP协议是手机应用上最广泛的协议,它可以通过传输层的TCP协议在客户端和服务器端之间进行数据以及数据之间的交互。
一个HTTP请求报文由请求行、请求头部、空行和请求数据4个部分组成。
HTTP请求报文格式如下:
网上关于HTTP的介绍特别多,我这里给大家分析一个最常见的应用场景,在浏览网页时,大家经常会碰到一些错误码,最常见的像404,这些都是HTTP的响应报文中的状态码。
常见网页浏览状态码如下:
关于网络通信的知识就讲解到这,没有进行过多的分析,但想必大家对网络通信是个啥,有哪些常见的通信协议应该有了个大致的了解。
若大家对某一块想要深入了解,网上有很多资料可供大家选择。
二、python--hello/hi网络聊天程序
在本地linux系统实现基于tcp的网络通信,服务端程序监听本地ip地址127.0.0.1,监听端口号为6666。客户端程序向本机该端口号发送消息。
鉴于python对socket通信api封装的很好,整个代码过程并不多,主要分析代码函数。
程序代码如下:
client.py:客户端代码
import socket import sys # 创建一个socket套接字 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 与对应的ip地址端口号建立连接 s.connect((‘127.0.0.1‘, 6666)) while True: #发送数据: try: data = input("客户端:") s.send(data.encode()) buf = s.recv(1024).decode() if buf != ‘exit‘: print("服务端: " + buf) except: print("Dialogue Over") s.close() sys.exit(0)
Server.py:服务端代码
import socket # 创建一个socket套接字,参数类型需要和client一致 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 监听端口 s.bind((‘127.0.0.1‘, 6666)) # 调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量 s.listen(1) sock, addr = s.accept() buf = sock.recv(1024).decode() while True: if buf != ‘exit‘: print("客户端: " + buf) data = input("服务端: ") sock.send(data.encode()) if data == ‘exit‘: break buf = sock.recv(1024).decode()
运行效果图:注意需要先运行server端的代码,不然程序会报错。
关键函数socket函数api解释:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM):socket.AF_INET代表使用IPV4通信,流式套接字SOCK_STREAM。
s.bind((‘127.0.0.1‘, 6666)):绑定主机名(127.0.0.1)、端口号(6666)
s.connect((‘127.0.0.1‘, 6666)):连接到指定socket的服务器
s.listen(1):开始监听客户端发来的消息,最大监听数量为1
sock, addr = s.accept() :接受TCP连接并返回:conn 和 address
conn:新的套接字对象,可以用来接收和发送数据;address是连接客户端的地址。
sock.send(data.encode()):发送TCP数据,将参数中的数据发送到连接的套接字。
buf = sock.recv(1024).decode():接受TCP套接字的数据。数据以字符串形式返回,指定要接收的最大数据量为1024。
s.close():关闭套接字,即中断了一次连接。
由以上可以看出tcp连接是长期连接,一次建立连接后,双方可以互相发送消息。而对应的HTTP是短连接,客户端发送请求后,便断开了和服务器的连接。
三、python socke接口与Linux socket api之间的关系
Linux的一个哲学:所有的东西都是文件。socket也不例外,可读,可写,可控制,可关闭的文件描述符。
Python socket相关api函数在Linux socket api 中的对应关系:
(1)创建socket套接字
python中使用s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 创建。
Linux socket系统调用创建一个socket方式如下
#include <sys/types.h> #include <sys/socket.h> int socket(int domain,int type, int protocol);
函数说明:
创建一个sockfd,以文件描述符的形式作为网络实体抽象的操作接口,包括unix domain socket, internet domain socket等不同场景下,可以按照文件读写方式来收发数据。
参数说明:
domain 表示使用何种底层协议族 , 比如PF_UNIX(alias PF_LOCAL, Unix domain socket),PF_INET (TCP/IPv4),PF_INET6 (TCP/IPv6)等。由于历史原因,地址族AF_*(包括AF_LOCAL)通常作为实际使用,AF_* 与PF_*对应同值;
type 表示指定服务类型,主要有SOCK_STREAM(TCP流服务)和SOCK_DGRAM(UDP数据报)等服务;
protocol 表示在前两个参数确定的协议集合下,进一步确认具体传输协议,比如:IPPROTO_TCP、IPPROTO_UDP等。由于前两个参数已经给出了足够的信息,该参数已经确定,因此通常该参数置为0即可。
返回值:
? 成功返回socket文件描述符;失败返回 -1, 并设置errno。
(2)绑定主机的套接字
python中使用s.bind((‘127.0.0.1‘, 6666))来绑定。
Linux socket系统调用创建一个socket方式如下
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
函数说明:
?将一个socket地址绑定到socket文件描述符上。客户端socket文件描述符通常不需要绑定socket地址,采用匿名方式即可。
参数说明:
sockfd 表示需要命名(绑定)的目标socket文件描述符;
my_addr 表示将socket地址绑定至sockfd,即命名该sockfd;
addrlen 表示该地址的长度。
返回值:
?成功时返回0, 失败时返回-1并设置errno, 常见的errno如下:
EACCES 被绑定的地址是受保护的地址(知名服务器端口:0~1023),仅超级用户能够访问;
EADDRINUSE 被绑定的地址正在使用中,比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
(3)开启监听
python中使用s.listen(1)来监听,1为最大监听数。
Linux socket系统调用创建一个socket方式如下:
函数说明:
? 在内核创建最大长度为backlog的监听队列。
参数说明:
sockfd 表示创建的socket 文件描述符;
backlog 表示提示内核监听队列中处于完全连接状态(ESTABLISHED)socket的最大长度,而半连接状态(SYN_RCVD)socket队列的最大长度定义在/proc/sys/net/ipv4/tcp_max_syn_backlog,若队列满,则peer客户端(connect())将收到ECONNREFUSED。
返回值:
?成功时返回0, 失败则返回-1, 并设置errno。常见的errno如下:
EADDRINUSE 已有其他socket监听该port. 当未命名sockfd时(极罕见,例RPC,客户端通过端口映射器获取到临时端口完成建立连接),通常会从/proc/sys/net/ipv4/ip_local_port_range端口范围内获取临时端口,会发生临时端口范围内的端口都被使用,此时返回该错。
本文章主要对这三个Linux socket api 进行了分析,要想了解更多,可参考链接:https://blog.csdn.net/alpslover/article/details/80387140
此外,可使用strace python server.py命令跟踪这个程序所使用的系统调用
最后,我的上一篇博客正好介绍了Netstat命令,该命令可以用于显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。
要想熟悉该命令,可详细参考我的上一篇博客。