socket网络编程

 

引言

计算机之间是如何通信的?

  同一台机器上的两个程序之间的通讯,就需要依赖文件。

  但是当你的a.py和b.py分别在不同电脑上的时候,你要怎么办呢?

  类似的机制有计算机网盘,qq等等。我们可以在我们的电脑上和别人聊天,可以在自己的电脑上向网盘中上传、下载内容。这些都是两个程序在通信。

  这个时候就需要用到网络:两台机器之间的两个程序之间的通讯,就必须依赖网络。

互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

mac地址

  又称为物理地址。

  head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址。

  mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)

IP地址与IP协议

  又成临时地址

  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1
    4位的点分十进制数  ipv4协议
    192.168.10.xxx
    0-255.0-255.0-255.0-255

    6位的点分十进制数  ipv6协议
    0-255.0-255.0-255.0-255.0-255.0-255

127.0.0.1:本地回环地址 本机的地址
0.0.0.0:ip地址的、回环地址的所有的用户都能找到你这台机器

外网ip 我们谁都能访问
内网ip 从外部不能访问,只能在内部环境中互相访问
外网ip永远不会跟内网IP冲突
    0.0.0.0-255.255.255.255 中间为内网保留了一些字段
    内网IP:
        192.168.0.0 - 192.168.255.255
        10.0.0.0 - 10.255.255.255
        172.16.0.0 - 172.31.255.255

    回环地址:指的是在我们测试过程中使用的一个地址
        127.0.0.1
    0.0.0.0 开发环境中

  

arp协议 ——查询IP地址和MAC地址的对应关系

  地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
  主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址。
  收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
  地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存;由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法到达预期的主机或到达错误的主机,这就构成了一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。相关协议有RARP、代理ARP。NDP用于在IPv6中代替地址解析协议。
 

子网掩码

  所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

  知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。

 

TCP协议和UDP协议

端口

  端口范围0-65535

  我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。

TCP协议

  当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。

  这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。

三次握手

 首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。

 “三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

四次挥手

 假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

 

 

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

 

 

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接。[1] 
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
TCP的三次握手
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。[1] 
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2] 
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。[2] 
TCP的四次握手

UDP协议

  当应用程序希望通过UDP与一个应用程序通信时,传输数据之前源端和终端不建立连接。

  当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。

TCP和UDP对比

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

 

互联网协议与osi模型

互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

每层运行常见物理设备

每层运行常见的协议

 

socket

socket层

 

 

 

socket通信

 

 

socket套接字使用

1.基于TCP协议的socket

tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)  #接收客户端信息
print(ret)       #打印客户端信息
conn.send(b'hi')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

  

client端

import socket
sk = socket.socket()           # 创建客户套接字
sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024)         # 对话(发送/接收)
print(ret)
sk.close()            # 关闭客户套接字

  

 

2.基于UDP协议的socket

udp是无链接的,启动服务之后可以直接接受消息不需要提前建立链接

server端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
udp_sk.close()                         # 关闭服务器套接字

  

client端

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

  

应用场景:

基于TCP协议的文件传输

import os
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()   # ('127.0.0.1',9000)

conn,addr = sk.accept()  # ('127.0.0.1',9000) ('127.0.0.1',49513)
# 先传文件名
file_path = r'D:\video\5.网络基础2.mp4'
filename = os.path.basename(file_path)
conn.send(filename.encode('utf-8'))

# 再传文件
file_size = os.path.getsize(file_path)
with open(file_path,'rb') as f:
    while file_size:
        content = f.read(1024)
        file_size -= len(content)
        conn.send(content)
conn.close()
sk.close()
服务端
import socket
# 下载
sk = socket.socket()
sk.connect(('127.0.0.1',9000)) # ('127.0.0.1',9000) ('127.0.0.1',97521)
filename = sk.recv(1024)
print('--> ',filename)
filename = filename.decode('utf-8')
with open(filename,'wb') as f:
    while True:
        content = sk.recv(1024)
        if content:
            f.write(content)
        else:break
sk.close()
客户端

基于UDP的多人聊天(QQ)

import socket
sk = socket.socket(type = socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',9001))
fiends = {'苑昊':'\033[1;32;40m苑昊 : %s\033[0m','哪吒':'\033[1;33;44m哪吒 : %s\033[0m'}
while True:
    msg,addr = sk.recvfrom(1024)
    user,msg = msg.decode('utf-8').split(':')
    if msg.strip() == 'q':
        continue
    else:
        print(fiends[user]%msg)
    send_msg = input('>>>').encode('utf-8')
    sk.sendto(send_msg,addr)

sk.close()
服务端
import socket

sk = socket.socket(type = socket.SOCK_DGRAM)
addr = ('127.0.0.1',9001)
user = '苑昊'
while True:
    send_msg = input('>>>')
    sk.sendto(('%s:%s'%(user,send_msg)).encode('utf-8'),addr)
    if send_msg == 'q':
        break
    msg,_ = sk.recvfrom(1024)
    print(msg.decode('utf-8'))

sk.close()
客户端1
import socket

sk = socket.socket(type = socket.SOCK_DGRAM)
addr = ('127.0.0.1',9001)
user = '哪吒'
while True:
    send_msg = input('>>>')
    sk.sendto(('%s:%s'%(user,send_msg)).encode('utf-8'),addr)
    if send_msg == 'q':
        break
    msg,_ = sk.recvfrom(1024)
    print(msg.decode('utf-8'))

sk.close()
客户端2

基于UDP协议的时间同步服务器

import time
import socket

sk = socket.socket(type = socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8848))
while True:
    msg,addr = sk.recvfrom(1024)
    fmt_time = time.strftime(msg.decode('utf-8'))
    sk.sendto(fmt_time.encode('utf-8'),addr)

sk.close()
服务端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)

sk.sendto('%Y-%m-%d %H:%M:%S'.encode('utf-8'),('127.0.0.1',8848))
msg,_ = sk.recvfrom(1024)
print(msg.decode('utf-8'))

sk.close()
客户端

 

socket参数详解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

 

 

粘包

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()

conn, addr = sk.accept()
conn.send(b'hello')
conn.send(b'world')
conn.close()

sk.close()  

client端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 9000))

ret1 = sk.recv(6)
print(len(ret1), ret1)
ret2 = sk.recv(5)
print(ret2)
sk.close()  

粘包现象

含包现象
    数据很短
    时间间隔短
拆包现象
    大数据会发生拆分
    不会一次性的全部发送到对方
    对方在接收的时候很可能没有办法一次性收到所有的信息
    那么没有接收完的信息很可能和后面的信息粘在一起
粘包现象只发生在tcp协议
    tcp协议的传输是流式传输
    每一条信息与信息之间没有边界的
    
udp协议中是不会发生粘包现象的
    适合短数据的发送
    不建议你发送过长的数据
    会增大你数据丢失的几率

在程序中会出现粘包:收发数据的边界不清晰
接收数据这一端不知道要接收数据的长度是多少。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

只有TCP有粘包现象,UDP永远不会粘包

粘包不一定会发生:

如果发生了:1.可能是在客户端已经粘了

      2.客户端没有粘,可能是在服务端粘了

客户端粘包:

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)

client端:
import socket
import time
 
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 
client.connect(('127.0.0.1',9904))
 
 
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
 
server端:
import socket
import time
 
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',9904)) #0-65535:0-1024给操作系统使用
server.listen(5)
 
 
conn,   addr=server.accept()
print('connect by ',addr)
res1 = conn.recv(100)
print('第一次',res1)
res2=conn.recv(10)
print('第二次', res2)


------
输出
connect by  ('127.0.0.1', 9787)
第一次 b'helloworld'
第二次 b''

  

服务端粘包

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

server端:
import socket
import time
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',9904)) #0-65535:0-1024给操作系统使用
server.listen(5)
 
 
conn,   addr=server.accept()
print('connect by ',addr)
res1 = conn.recv(2)#第一没有接收完整
print('第一次',res1)
time.sleep(6)
res2=conn.recv(10)# 第二次会接收旧数据,再收取新的
print('第二次', res2)
 
client端
import socket
import time
 
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 
client.connect(('127.0.0.1',9904))
 
 
client.send('hello'.encode('utf-8'))
time.sleep(5)
client.send('world'.encode('utf-8'))


------
输出
connect by  ('127.0.0.1', 10184)
第一次 b'he'
第二次 b'lloworld'

  

解决粘包现象

我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

使用struct模块可以用于将Python的值根据格式符,转换为字符串(byte类型)

struct模块中最重要的三个函数是pack(), unpack(), calcsize()

pack(fmt, v1, v2, ...)     按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)

unpack(fmt, string)       按照给定的格式(fmt)解析字节流string,返回解析出来的tuple

calcsize(fmt)                 计算给定的格式(fmt)占用多少字节的内存

>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

  

struct中支持的格式如下表:

 

验证客户端链接的合法性

如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现

import os
import hmac
import socket

def auth(conn):
    msg = os.urandom(32)  # 生成一个随机的字符串
    conn.send(msg)  # 发送到client端
    result = hmac.new(secret_key, msg)  # 处理这个随机字符串,得到一个结果
    client_digest = conn.recv(1024)  # 接收client端处理的结果
    if result.hexdigest() == client_digest.decode('utf-8'):
        print('合法连接')  # 对比成功可以继续通信
        return True
    else:
        print('不合法连接')  # 不成功,close
        return False

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
secret_key = b'luffy'
conn, addr = sk.accept()
if auth(conn):
    print(conn.recv(1024))
    conn.close()
else:
    conn.close()
sk.close()
server
import hmac
import socket
def auth(sk):
    msg = sk.recv(32)
    result = hmac.new(key, msg)
    res = result.hexdigest()
    sk.send(res.encode('utf-8'))

key = b'luffy'
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
auth(sk)
sk.send(b'upload')
# 进行其他正常的和server端的沟通
sk.close()
client

 

socketserver

用TCP协议通过socketserver实现即时通讯

import socketserver
# TCP协议的server端不需要导入socket
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        conn = self.request
        while True:
            conn.send(b'hello')
            print(conn.recv(1024))

# 创建一个server,将服务器地址绑定到(地址,端口)
server = socketserver.ThreadingTCPServer(('127.0.0.1', 9000), Myserver)
server.serve_forever()  # 让server永远运行下去,除非强制停止程序
socketserver
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
while True:
    ret = sk.recv(1024)
    print(ret)
    sk.send(b'bye')
sk.close()
client

 

 

 

 

 


  

 

  

 

 

 

转载于:https://www.cnblogs.com/eaoo/p/9637600.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值