# socket

socket

1. 概述

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

# socket

3. socket基础

2. socket聊天室代码

  1. socket server代码的步骤一般如下

    # socket

    1. 服务器端

      1. 服务器首先初始化好socket
      2. 绑定这个socket服务器运行的ip地址和端口
      3. 监听这个服务器
      4. 等待socket连接,这个步骤会一直阻塞,直到有socket链接进来
      5. 当连接成功之后,服务器就可以读取客户端发送的socket消息
      6. 服务器可以选择给客户端回传socket消息
      7. 服务器关闭socket
    2. 客户端

      1. 客户端初始化socket
      2. 客户端去和服务器建立链接
      3. 客户端向服务器发送数据
      4. 客户端等待服务器相应数据
      5. 客户端关系socket
    3. 按照上述描述,python代码(伪)书写应该如下

        ss = socket() 
        ss.bind() 
        ss.listen() 
        inf_loop:
			  cs = ss.accept() 
			  comm_loop:
			      cs.recv()/cs.send() 
			      cs.close()
			  ss.close()
  1. python socket库基础知识

    1. 要创建套接字,必须使用 socket.socket()函数,它一般的语法如下。 socket(socket_family, socket_type, protocol=0)。socket.socket是new出来socket套接字对象,但是通信的运输层有tcp或者upd来通信. 网络层有ip协议来传输和本地[非网络的 AF_LOCAL/AF_UNIX]套接字来传输数据。所以我们需要配置socket_family和socket_type

    2. 套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

      1. socket_family

         1. AF_INET表示使用ipv4协议来传输
         2. AF_UNIX表示使用非网络的套接字来传输
        
      2. socket_type

        1. SOCK_STREAM 表示tcp协议
        2. SOCK_DGRAM 表示使用upd协议来连接
        
      3. 综合:

        所以说 tcpSock = socket(AF_INET, SOCK_STREAM)表示使用ip和tcp来进行通信
        
    3. s.bind():
      将地址(主机名、端口号对)绑定到套接字上

    4. ss.listen():

      设置并启动 TCP 监听器

    5. ss.accept():

      被动接受 TCP 客户端连接,一直等待直到连接到达(阻塞)

    6. s.recv():

      接受socket消息,请注意 上面的方法是接受连接

    7. s.send():

      发送 TCP 消息

    8. s.close():

      关闭套接字

  2. 服务器/客户端实际代码v1(简单的流程演示):

  3. 服务器代码:

          import socket

			bind_address = ("0.0.0.0",19527) #或者127.0.0.1本地回环网,0.0.0.0是默认换为内网的网址。			
			#申明
			ss = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
			#绑定地址和监听
			ss.bind(bind_address)
			ss.listen()
			#获取客户端socket对象和地址
			cli,addr = ss.accept()
			#获取tcp消息 一次1kb
			msg = cli.recv(1024)
			print(msg)
			#回送消息
			cli.send(b'get')
			ss.close()
  1. 客户端代码
import socket
			send_address = ("0.0.0.0",19527)
			cli = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
			#直接连接
			cli.connect(send_address)
			#发送数据
			cli.send(b'teset')
			#接受数据
			msg = cli.recv(1024)
			print(msg)
			cli.close()
  1. 服务器/客户端代码(更有交互性的代码)

    1. 说明:
      这次的代码,让服务器和客户端更有交互性,客户端输入消息后,服务器返回消息

    2. 代码:
      服务端:

import socket

		ss_address = ("0.0.0.0",19534)		#地址linux用0.0.0.0:n  实验,
												#windows用本机地址cmd中输出 ipconfig 查看
		ss = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
												#socket 连接对象,STREAM(stream)数据流 
												#socket.AF_INET使用ip4协议  socket.AF_INET6使用ip6
		ss.bind(ss_address)					#占用一个端口,ip 0.0.0.0:19534 本地用
		ss.listen(2)							#对端口进行监听,实时接收客户端的请求内容
		while True:							#建立循环,
		    cli,addr = ss.accept()	       #注意:accept()函数会返回一个元组
												#元素1为客户端的socket对象,元素2为客户端的地址(ip地址,端口号)
		    print('收到来自ip为{}的连接'.format(addr))		#提示连接成功
		    									
		    #在连接的时候,进入循环
		    while True:
		        # 接受消息
		        data = cli.recv(1024)
		        if not data:					#当接收的内容为空不存在的时候,
		            break					#跳出最近的一次循环
		        print("发送请求内容为:{}".format(data.decode("utf-8")))
		        re_msg = "收到你的消息{}".format(data.decode('utf8'))  #定义变量接收客户端的消息
		        cli.send(re_msg.encode('utf8'))				#再将内容传给客户端
		    cli.close()

客服端:


import socket

cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  #创建一个名为cli客户端的socket对象
send_address =  ("0.0.0.0",19534)			#定义变量send_address 储存ip和端口
cli.connect(send_address)   			#连接服务端
while True:					
    neirong = input("输入内容:")		#输入传送的内容
    cli.sendall(bytes(neirong, encoding="utf8"))	#编码并且传送内容刚给服务端
    data = cli.recv(1024)							#接收服务端返回的内容
    if not data:										#判断为不为空
        break
    print(data.decode("utf8"))						#把服务端返回内容解码并打印
cli.close()
  1. 客户端接受http请求,并且返回http代码

    1. 说明:
      我们在获取http代码的时候,获取http代码的库文件,或者浏览器底层,就是用了socket来获取http报文,我们可以模拟底层的报文请求方式,来深一步的了解http报文结构

    2. 代码:

	# 使用socket获取http报文
	import socket
	from urllib.parse import urlparse
	
	# 初始化socket
	ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	domain = "https://www.baidu.com"
	url = urlparse(domain)
	host = url.netloc
	path = url.path
	if path == "":
	    path = "/"
	ss.connect((host, 80))
	#'\r'的本意是回到行首,'\n'的本意是换行
	data = "GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\nUser-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36\r\n\r\n".format(
	    path, host).encode('utf8')
	ss.send(data)
	res = b""
	#报文就不像聊天数据那么小了 需要循环的拼接报文
	while True:
	    d = ss.recv(1024)
	    if d:
	        res += d
	    else:
	        break
	print(res.decode('utf8'))
	ss.close()
  1. 多线程聊天代码演示

    1. 说明: 刚才的版本只能接受一个连接,我们可以使用多线程版本来优化代码

    2. 代码:

import socket
		import threading
		
		def cli_msg(cli,addr):
		    print('收到来自ip为{}的连接'.format(addr))
		    #在连接的时候,进入循环
		    while True:
		        # 接受消息
		        data = cli.recv(1024)
		        if not data:
		            break
		        re_msg = "收到你的消息{}".format(data.decode('utf8'))
		        cli.send(re_msg.encode('utf8'))
		    cli.close()
		    return True
		
		ss_address = ("0.0.0.0",19534)
		ss = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
		ss.bind(ss_address)
		ss.listen(2)
		
		
		while True:
		    #获取连接
		    cli,addr = ss.accept()
		    threading.Thread(target=cli_msg,args=(cli,addr)).start()
import socket

		ss_address = ("0.0.0.0",19534)
		
		ss = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
		
		ss.connect(ss_address)
		
		while True:
		    data = input('请输入消息:')
		    if not data:
		        break
		    ss.send(data.encode('utf8'))
		    print(ss.recv(1024).decode('utf8'))
		
		
		ss.close()

多人聊天多线程

(1)

# 1. 服务端


from queue import Queue                             #安全队列
import socket       #协议库
from concurrent.futsure import ThreadPoolExecutor       #多线程标准模块concurrent.futsure
                                                        # 线程池ThreadPoolExecutor

class Server:
    """
    1. 接收多人的链接进来
    2. 收到一个人的消息之后,把消息广播给所有人
    """

    def __init__(self, bind_ip='0.0.0.0', port=21805):
        """
        初始化函数
        :param bind_ip:
        :param port:
        """
        self.bind_address = (bind_ip, port)# 初始化绑定ip
                        # 初始化的tcp链接, socket.AF_INET使用ip4协议,socket.SOCK_STREAM对象数据流
        self.ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ss.bind(self.bind_address)  #服务端绑定ip和端口
        self.ss.listen(10)                #进行端口监听
        self.pools = ThreadPoolExecutor(20)     #设置多线程,和线程数上限为20
        """
        广播
        """
        self.msg_queue = Queue()              #初始化一个安全队列
        """
        ip榜单
        """
        self.ip_list = []           #初始化ip的list
        self.black_list = []        #初始化黑名单的ip的list
        # 所有人链接进来的表
        self.conns = []             #所有人的链接对象

    def start(self):
        """
        接收链接
        :return:
        """
        self.pools.submit(self.send_msg)        #启动线程池,运行send_msg函数,即发送消息,广播
        while True:
            conn, addr = self.ss.accept()       #获取链接字节套对象 (接收和发送数据),地址ip
            if self.is_black(addr):             #判断ip信息是否存在黑名单的list里面
                continue
            self.pools.submit(self.recv_msg, conn, addr)    #启用线程池,运行recv_msg,即接收消息
            self.conns.append(conn)

    def recv_msg(self, conn, addr):              #接收消息
        while True:
            msg = conn.recv(1024)           #每一次接收的小大 1024b
            if (not msg) or (len(msg) == 0):    #判断有误内容
                self.remove_conn(conn,addr) #无内容调用函数remove_conn
            self.msg_queue.put((msg, addr)) #有内容,则添加msg和addr入安全队列
            print('收到ip为{}的消息{}'.format(addr, msg.decode()))    #打印

    def send_msg(self):
        while True:
            msg, addr = self.msg_queue.get()
            ip, port = addr
            for conn in self.conns:
                try:
                    conn.send("ip为{},port为{},说:{}".format(ip, port, msg.decode()).encode())
                except Exception:
                    self.remove_conn(conn,addr)

    def remove_conn(self,conn,addr):        #用户退出聊天,删除他的list里面的信息
        conn.close()
        self.conns.remove(conn)             #从conns 列表中删除用户信息
        msg = '我已经退出聊天'.encode()
        self.msg_queue.put((msg, addr))     #添加安全队列,进行广播

    def is_black(self, addr):
        """
        判断黑名单用户
        :return:
        """
        ip, port = addr
        if ip in self.black_list:
            return True
        else:
            return False


if __name__ == "__main__":
    ss = Server()
    ss.start()

# 2. 客户端
import socket       #协议库
from concurrent.futsure import ThreadPoolExecutor       #多线程标准模块concurrent.futsure
                                                        # 线程池ThreadPoolExecutor

class Client:

    def __init__(self,ip='0.0.0.0',port=21805):
        self.ip = ip
        self.port = port
        self.bind_ip = (ip, port)
        self.ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                                                        # socket 连接对象,STREAM(stream)数据流
                                                        # socket.AF_INET使用ip4协议  socket.AF_INET6使用ip6
        self.pools = ThreadPoolExecutor(5)          #启用最大五个线程

    def start(self):
        self.ss.connect(self.bind_ip)       #客户端连接服务端ip和端口
        self.pools.submit(self.send)        #启用线程池运行send函数,发送消息
        self.pools.submit(self.recv)        #启用线程池运行recv函数,接受消息


    def send(self):
        while True:
            msg = input('输入消息:')
            self.ss.send(msg.encode())
    def recv(self):
        while True:
            msg = self.ss.recv(1024)
            print('收到消息{}'.format(msg.decode()))


if __name__ == "__main__":
    c = Client()
    c.start()

(2)

"""
1. 多人聊天室
2. 服务端可以接受多个客户端的链接
3. 客户端发送的消息可以通过服务端广播给所有人
"""
import socket
from concurrent.futures import ThreadPoolExecutor


class Server:

    def __init__(self, bind_ip='0.0.0.0', port=30001, listen_num=25):
        """
        1. 初始化socket服务

        """
        # 初始化tcp链接
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 绑定地址
        self.bind_address = (bind_ip, port)
        self.server.bind(self.bind_address)
        # 默认放25个客户端进来
        self.server.listen(listen_num)
        # 所有的客户端链接都放在这里面
        self.conns = list()
        self.pools = ThreadPoolExecutor(listen_num + 5)
        # 字典 对应 conn对应的线程池
        self.conn2pool = dict()

    def accept(self):
        """
        接受客户端的链接
        :return:
        """
        conn, addr = self.server.accept()
        self.conns.append(conn)
        return conn, addr

    def multiple_accept(self):
        """
        接受多个客户端链接
        :return:
        """
        while 1:
            conn, addr = self.accept()
            # 让recv_msg 不阻塞 每个conn(链接都有一个独立的线程去收消息)
            pool = self.pools.submit(self.recv_msg, conn, addr)
            print(addr)
            self.conn2pool.update({conn:pool})

    def recv_msg(self, conn, addr):
        """
        1. 一个客户端接受消息
        :param conn:
        :return:
        """
        while 1:
            msg = conn.recv(1024)
            if (not msg) or len(msg) == 0:
                """
                当消息为空或者没有消息还发送过来的时候,断开链接
                """
                self.break_conn(conn)
            self.broadcast_msg(msg, addr)

    def broadcast_msg(self, msg, addr):
        """
        广播消息
        :return:
        """
        for conn in self.conns:
            self.send_msg(conn, msg, addr)

    def send_msg(self, conn, msg, addr):
        """
        发送消息
        :param msg:
        :return:
        """
        msg = self.handle_msg(msg, addr)
        try:
            conn.send(self.str2bytes(msg))
        except Exception:
            self.break_conn(conn)

    def bytes2str(self, msg):
        """
        二进制转字符串
        :param msg:
        :return:
        """
        try:
            return msg.decode()
        except Exception:
            return msg

    def str2bytes(self, msg):
        """
        字符串转二进制
        :param msg:
        :return:
        """
        try:
            return msg.encode()
        except Exception:
            return msg

    def handle_msg(self, msg, addr):

        msg_str = "用户---{}--说:{}".format(addr, self.bytes2str(msg))
        print(msg_str)

        return msg_str

    def break_conn(self, conn):
        conn.close()
        self.conns.remove(conn)
        #回收线程
        pool = self.conn2pool[conn]
        print('{}断开链接'.format(conn))
        pool.shutdown()

    def start(self):
        print('server start')
        self.multiple_accept()
        print('server end')

if __name__ == '__main__':
    s = Server()
    s.start()

import socket
from concurrent.futures import ThreadPoolExecutor


class Client:

    def __init__(self, ip='10.2.0.78', port=30001):   #自己的机子的ip即可,
        self.ip = ip
        self.port = port
        self.bind_ip = (ip, port)
        self.ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.pools = ThreadPoolExecutor(5)

    def start(self):
        self.ss.connect(self.bind_ip)
        self.pools.submit(self.send)
        self.pools.submit(self.recv)

    def send(self):
        while True:
            msg = input('输入消息:')
            self.ss.send(msg.encode())


    def recv(self):
        while True:
            msg = self.ss.recv(1024)
            if (not msg) or len(msg) == 0:
                """
                当消息为空或者没有消息还发送过来的时候,断开链接
                """
                self.ss.close()
                break
            print('收到消息{}'.format(msg.decode()))
        print('链接已经断开')


if __name__ == "__main__":
    c = Client()
    c.start()