ubuntu系统下使用python3实现简单的网络聊天程序

这是我的第二篇博客,很遗憾第一篇博客没有得到应有的认可。

可能是因为原理介绍和实操部分不够多,只是单纯分析了某一条指令在打开网页过程中,输出的变化。

在我的第二篇博客中把相关原理介绍的更加详细了,同时丰富了程序代码部分的介绍。

本文对通信相关知识点(如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协议相关的统计数据,一般用于检验本机各端口的网络连接情况。

要想熟悉该命令,可详细参考我的上一篇博客。

 

 

 

posted @ 2019-12-09 18:08  一只猫的旅行~~  阅读(892)  评论(0编辑  收藏  举报