socket编程基本流程及TCP套接字

socket编程基本流程及TCP套接字

一 socket编程基本流程

1 套接字的概念及分类

1.1 套接字是什么

	套接字是一种【通信机制】,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux所提供的功能(如打印服务,ftp等) 通常都是通过套接字来进行通信的,套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分出来,套接字可以实现将多个客户连接到一个服务器。

	套接字,也称为BSD套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。

	简单的举例说明:Socket=Ip address+ TCP/UDP + port。

1.2 套接字的分类

# (1) 基于文件类型
	套接字家族的名字:AF_UNIX。
# (2) 基于网络类型
	套接字家族的名字:AF_INET。

2 套接字工作流程

2.1 面向连接的套接字Socket通信工作流程

1).服务器先用socket函数来建立一个套接字,用这个套接字完成通信的监听;

2).用bind函数来绑定一个端口号和一个IP地址。因为本地计算机可能有多个网址和IP,每一个IP有多个端口,需要指定一个IP和端口进行监听。

3).服务器调用listen函数,使服务器的这个端口和IP处于监听状态,等待客户机的连接。

4).客户机用socket函数建立一个套接字,设定远程IP和端口。

5).客户机调用connect函数连接远程计算机指定的端口。

6).服务器用accept函数来接受远程计算机的连接,建立起与客户机之间的通信。

7).建立连接以后,客户机用write函数想socket中写入数据。也可以用read函数读取服务器发送来的数据。

8).服务器用read函数读取客户机发送来的数据,也可以用write函数来发送数据。

9).完成通信以后,用close函数关闭socket连接。

2.2 面向无连接的套接字Socket通信工作流程

	无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有简历连接的过程。进行通信之前,需要简历网络套接字。服务器需要绑定一个端口,在这个端口上监听收到的信息。客户机需要设置远程IP和端口,需要传递的信息需要发送到这个IP和端口上。

2.3 socket() 模块函数用法

2.3.1 socket() 模块函数基本用法
import socket
socket.socket(socket_family, socket_type, protocal=0)
# socket_family 可以是 AF_UNIX 或 AF_INET;
# socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM;
# protocal 一般不填,默认值为0

# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 由于 socket 模块中有太多属性我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
# 例如:tcpSock = socket(AF_INET, SOCK_STREAM)
2.3.2 服务端套接字函数
s.bind()        	# 绑定(主机、端口号) 到套接字
s.listen()      	# 开始TCP监听
s.accept()      	# 被动接受TCP客户的连接·(阻塞式) 等待连接的到来
2.3.3 客户端套接字函数
s.connect()         # 主动初始化TCP服务器连接
s.connect_ex()      # connect()函数的扩展版本,出错时返回错误代码,而不是抛出异常
2.3.4 公共用途的套接字函数
s.recv()             # 接受TCP数据
s.send()             # 发送TCP数据(send在待发送数据量大于已己端缓存区剩余空间时,数据丢失,不会发完) 
s.sendall()          # 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) 
s.recvfrom()         # 接收UDP数据
s.sendto()           # 发送UDP数据
s.getpeername()      # 连接到当前套接字的远端地址
s.getsockname()      # 当前套接字的地址
s.getsockopt()       # 返回指定套接字的参数
s.setsockopt()       # 设置指定套接字的参数
s.close()            # 关闭套接字
2.3.5 面向锁的套接字方法
s.setblocking()       # 设置套接字阻塞与非阻塞模式s.settimeout()        # 设置阻塞套接字操作的超时时间s.gettimeout()        # 得到阻塞套接字的操作超时时间
2.3.6 面向文件的套接字函数
s.fileno()            # 套接字的文件描述符s.makefile()          # 创建一个与该套接字相关的文件

二 基于TCP的套接字

1 基本模板

1.1 tcp服务端及特性

ss = socket() 			# 创建服务器套接字ss.bind()      			# 把地址绑定到套接字ss.listen()      		# 监听链接inf_loop:      			# 服务器无限循环    cs = ss.accept() 	# 接受客户端链接    comm_loop:         	# 通讯循环        cs.recv()/cs.send() # 对话(接收与发送)    cs.close()    		# 关闭客户端套接字ss.close()        		# 关闭服务器套接字(可选)

1.2 tcp客户端

cs = socket()    			# 创建客户套接字cs.connect()    			# 尝试连接服务器comm_loop:        			# 通讯循环    cs.send()/cs.recv()    	# 对话(发送/接收)cs.close()            		# 关闭客户套接字

2 一步步实现TCP套接字

2.1 基于tcp协议实现简单套接字通信

##——————————————————————————————————————server端基础版本import socket# 1.买手机phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM =>TCP协议# 2.插手机卡phone.bind(('127.0.0.1', 8080))  # 本地回环,使用一个元组传参# 3.开机phone.listen(5)print('starting %s:%s' % ('127.0.0.1', 8080))# 4.等电话连接conn, client_addr = phone.accept()# 5.收/发消息data = conn.recv(1024)  # 最大接收的字节数print('收到的客户端数据:', data.decode('utf-8'))conn.send(data.upper())# 6.关闭conn.close()phone.close()##——————————————————————————————————————client端基础版本import socket# 1.买手机phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM => TCP协议# 2.拨电话phone.connect(('127.0.0.1', 8080))# 3.发/收消息phone.send('hello'.encode('utf-8'))data = phone.recv(1024)print('服务的返回数据:', data.decode('utf-8'))# 4.关闭phone.close()

2.2 加上链接循环与通信循环

2.2.1 为什么要加上加上链接循环与通信循环?
# 因为服务端应满足的特性:​	① 一直对外提供服务;​	② 并发地提供服务;
2.2.2 加上链接循环与通信循环
##——————————————————————————————————————server端基础版本加上链接循环与通信循环import socket# 1.买手机phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM =>TCP协议# 2.插手机卡phone.bind(('127.0.0.1', 8080))  # 本地回环,使用一个元组传参# 3.开机phone.listen(5)  # 半连接池数量,队列print('starting %s:%s' % ('127.0.0.1', 8080))# 4.等电话连接===>>>链接循环(D)   # ##—————————————————此处的链接循环以后不可如此使用(需拆分开),这样只能一次处理一条服务,不能高并发while True:    conn, client_addr = phone.accept()    print(client_addr)    # 5.收/发消息===>>>通信循环(A)     while True:        try:            data = conn.recv(1024)  # 最大接收的字节数            # linux系统的一直收空信息的错误解决方法,判断是否为空,然后打破循环            if len(data) == 0:                break            # (B)  conn是一个双向连接,若客户端非正常断开,则服务端此处会报错,因为通信无法完成,需加入异常管理            print('收到的客户端数据:', data.decode('utf-8'))            conn.send(data.upper())        except Exception:  # windows系统的解决异常方法            break            # (C)  异常捕捉完成后,服务端不能结束,需要重新进行服务,则需要重新建立连接    # 6.关闭    conn.close()  # 用于回收收无用地异常关闭地链接,然后进入连接循环,重新监听客户端地请求,暂无并发效果phone.close()# 用于软件正常关闭使用,暂时无用,正常写软件可使用关闭按钮实现##——————————————————————————————————————client端基础版本加上链接循环与通信循环import socket# 1.买手机phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM => TCP协议# 2.拨电话phone.connect(('127.0.0.1', 8080))# 3.发/收消息===>>>通信循环while True:    msg = input('>>>:').strip()    # 用户输入消息进行通信    phone.send(msg.encode('utf-8'))    data = phone.recv(1024)    print('服务的返回数据:', data.decode('utf-8'))# 4.关闭phone.close()
2.2.3 会遇到的问题以及解决方案
2.2.3.1 问题:报错地址仍在使用

	原因:由于服务端仍然存在第四次挥手的**time_wait**状态在占用地址(相关知识:1.tcp三次握手,四次挥手;2.syn洪水攻击;3.服务器高并发情况下会有大量的time_wait状态的优化方法) 
2.2.3.2 解决方案
###### (A) Windows下,加入一条socket配置,重用ip和端口  (此方法不推荐,会导致套接字不知道去哪,换端口号相对更好一些) # 在服务端的绑定信息前面添加一条配置信息# 2.插手机卡phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加phone.bind(('127.0.0.1', 8080))  # 本地回环,使用一个元组传参
###### (B) Linux下,增加内核相关的配置,解决根本问题
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

2.3 半连接池

	未得到服务端连接的请求,都储存在半连接池中,当半连接池队列达到最大之后,后续的请求将无法得到响应,无法连接到服务器,除非半连接池队列数量减少,才能有新的请求进入半连接池。
​	并发量大的情况下,应该扩大半连接池,写入配置文件,使可调整,但是半连接池容量不能无限大,不可超过物理内存。

2.4 远程执行命令

# 改写成可以远程执行命令的版本
	# 使用subprocess模块,增加远程连接的功能
    
##——————————————————————————————————————server端基础版本加上链接循环与通信循环,再添加远程执行命令
import socket
import subprocess

# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM =>TCP协议

# 2.插手机卡
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加

phone.bind(('127.0.0.1', 8080))  # 本地回环,使用一个元组传参

# 3.开机
phone.listen(5)  # 半连接池数量,队列
print('starting %s:%s' % ('127.0.0.1', 8080))

# 4.等电话连接===>>>链接循环
while True:
    conn, client_addr = phone.accept()
    print(client_addr)
    # 5.收/发消息===>>>通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # 最大接收的字节数
            # linux系统的一直收空信息的错误解决方法,判断是否为空,然后打破循环
            if len(cmd) == 0:
                break

            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )
            res = obj.stdout.read()+obj.stderr.read()  # 为何拼接?
            print(res)
            conn.send(res)
        except Exception:  # windows系统的解决异常方法
            break
            # (C)  异常捕捉完成后,服务端不能结束,需要重新进行服务,则需要重新建立连接
    # 6.关闭
    conn.close()  # 用于回收收无用地异常关闭地链接,然后进入连接循环,重新监听客户端地请求,暂无并发效果
phone.close()
# 用于软件正常关闭使用,暂时无用,正常写软件可使用关闭按钮实现

##——————————————————————————————————————client端基础版本加上链接循环与通信循环,再添加远程执行命令
import socket

# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM => TCP协议

# 2.拨电话
phone.connect(('127.0.0.1', 8080))

# 3.发/收消息===>>>通信循环
while True:
    cmd = input('[root@localhost]# ').strip()    # 用户输入消息进行通信
    phone.send(cmd.encode('utf-8'))
    data = phone.recv(1024)
    # windows系统下,应该使用gbk解码
    print(data.decode('gbk'))

# 4.关闭
phone.close()
posted @ 2021-06-25 11:44  越关山  阅读(1130)  评论(0)    收藏  举报