Python50_网络编程(UDP、TCP)

网络编程基础

网络

  • 使用网络能够把多方链接在一起,然后可以进行数据传递
  • 所谓的就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信

ip地址

  • 目的:用来标记网络上的一台电脑(不允许重复)

  • 如何查看当前电脑的ip:

    • Linux:ifconfig,中的inet项
    • Windows:ipconfig
  • ip地址的分类

    • ipv4:v4就是第四个版本,由四部分组成,eg:192.168.14.91,每组数里面的最大值为255,最小值为0。通常,前三部分为网络号,最后一位为主机号,前三部分均相同即表示电脑在同一个网络下,主机号的0和255不能使用(是用于回路测试的)。
      分类:
      如果用仅用第一部分作为网络号,后三部分为主机号,则为A类地址;
      如果使用前两部分作为网络号,后两部分为主机号,则为B类地址;
      前三部分作为网络网,后一部分作为主机号则为C类地址【最常用】;
      D类地址用于多点广播,第一字节以1110开始,是一个专门的保留地址,它并不指向特定的网络,多点广播地址用来一次寻址一组计算机s地址范围为224.0.0.1-239.255.255.254;
      E类ip地址以1111开始,为将来使用保留,进作实验和开发用。
      私有ip:在这么多网络中,国际规定有一部分ip地址是用于我们的局域网使用,也就是属于私网ip。他们的范围是10.*和172.*以及192.*

    • ipv6:第六个版本,ip1235均为实验版本

端口

端口号也是一个数字,用来对电脑上的进程进行标识(从0-65536(2的16次方)的整数)

  • 分类

    • 知名端口:众所周知的端口(0-1023),eg:80分配给HTTP服务,21分配给FTP服务,即大于1024的端口随便使用
    • 动态端口:从1024-65535,之所以称之为动态端口,是因为它一般不固定分配,而是动态分配(当一个系统程序或应用程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用,当程序关闭时,同时也就释放了所占用的端口号)

socket简介

socket:套接字

socket是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:
它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于socke来完成通信的,例如QQ,收发email

在不同的语言中,socket的流程是一样的

创建一个socket(tcp套接字)

import socket

skt = socket.socket(socket.AF_INEF, socket.SOCK_STREAM)#创建tcp套接字,因为tcp协议是流式传输,所以第二个参数为SOCK_STREAM
#第一个参数为协议族,AF_INEF表示使用的是ipv4的协议族
#socket.socket()是一个类,此语句创建了一个套接字对象

#这里是使用套接字的功能(省略)

skt.close() #不用的时候关闭套接字

创建一个udp socket(udp套接字)

import socket
udp_skt = socket.socket(socket.AF_INEF, SOCK_DGRAM) #因为udp协议传输的为报文,所以第二个参数为SOCK_DGRAM

#使用套接字的功能(省略)

udp_skt.close() #不用的时候,关闭

UDP

UDP(User Datagram Protocol)通信模型中,不需要建立相关的链接,只需要发送数据即可,类似于生活中“写信”——不安全

udp发送数据demo

from socket import *

#1.创建套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)

#2.准备接受方地址
#'192.168.205.11'表示目的ip
#8080表示目的端口
dest_ip = "192.168.205.1"
dest_port = 8080
dest_addr = (dest_ip, dest_port)    #对方的ip和端口用元组

#3.从键盘获得数据
send_data = input(">")

#4.发送数据到指定电脑上的指定程序中
udp_socket.sendto(send_data.encode("utf-8"), dest_addr) #两个参数:要发送的内容,接受方地址

#关闭套接字
udp_socket.close()

可配合网络调试助手接收到以上程序发送的消息

udp接收数据demo

如果程序要接收数据,必须要一个固定的端口:使用bind绑定

客户端:

from socket import  *

HOST = "192.168.0.113"  #因为是自己写程序在本机上验证,所以这里的ip就是本机ip
PORT = 8888

BUFSIZE = 1024
ADDR = (HOST, PORT)

udpCliSock = socket(AF_INET, SOCK_DGRAM)    #第一个参数表示网络编程,第二个参数表示报文传输
#udpCliSock.bind(("",7788)) #可以使用此句给自己绑定端口,这样就不会使用系统随机分配的端口
while True:
    data = input(">")
    if not data:
        break   #如果没有输入数据,跳出循环,结束程序
    udpCliSock.sendto(data.encode("utf-8"),ADDR)    #将数据发送给目的地,
    # 因为在发送数据之前没有给自己绑定端口号,所以系统会随机分配一个端口号
    data, ADDR = udpCliSock.recvfrom(BUFSIZE)   #BUFSIZE表示本次接收的最大字节数
    #接收从服务器端返回的数据,recvfrom在数据没有到来的时候会阻塞

    if not data:
        break

    print(data.decode("utf-8"))

udpCliSock.close()

服务器端:

from socket import *
import time

HOST = ""   #本机ip,因为是做服务器,所以可以不用写,表示本机的任意一个ip
PORT = 8888 #端口号,不是小于1024的都可以
BUFSIZ = 1024   #缓冲区大小
ADDR = (HOST, PORT) 


udpServSock = socket(AF_INET, SOCK_DGRAM)   #获得一个socket

udpServSock.bind(ADDR)  #绑定本堤的相关信息(ip与端口),如果一个网络程序不绑定,则系统会随机分配

while True:
    print("waitting for message...")
    data, addr = udpServSock.recvfrom(BUFSIZ)   #bufsize表示本次接收的最大字节数,recvfrom的返回值为数据和发送端的地址(ip与端口组成的元组)
    #程序会在这里一直等待,直到接收到数据
    data = data.decode("utf-8")
    content = '[%s] %s' %(bytes(time.ctime(),"utf-8"), data)
    print("收到数据:", data)
    udpServSock.sendto(content.encode("utf-8"), addr)   #把收到的数据发送回客户端
    print("received from and return to:", addr)

udpServSock.close()

注意:严格来说,UDP并没有服务器和客户端的概念,想要收就recfrom,想要发就sendto

ps:代码不是从上到下写的,而是用到什么导入什么,用到什么定义什么

注意:同一个端口在同一时刻只能被一个程序所使用

  • 单工:要么只能收数据,要么只能发数据
  • 半双工:可以发送也可以接收数据,但是同一时刻只能收或者发
  • 全双工:同一时刻既可以发也可以收

TCP(更常用)

简介:

  • TCP协议,传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • TCP通信需要经过创建连接、数据传送、终止连接三个步骤
  • TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中“打电话”

TCP和UDP的不同点:

  • 面向连接
  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无差错的数据传输
  • 阻塞/流量控制

TCP注意点

  1. tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
  2. tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip,port等信息就好,本地客户端可以随机
  3. tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
  4. 当客户端需要连接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
  5. 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
  6. listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
  7. 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
  8. 关闭accept返回的套接字意味着这个客户端已经服务完毕
  9. 当客户端的套接字调用close后,服务器端会recv接堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线

ps:tcp有三次握手和四次挥手 (由操作系统底层自己完成)

客户端:

from socket import *


def creat_link():
    """
    建立链接
    :return: 套接字
    """
    # 1. 创建tcp的套接字,ps:客户端一般不绑定端口号,但是可以绑定
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    #tcp_socket.setsockopt(socket.SOL__SOCKET, socket.SO_REUSERADDR, 1) # 让服务器端可以先close,而不会出现地址正在使用中的问题(具体涉及到四次挥手)
    # 2. 链接服务器
    server_ip = input("server ip:")
    server_port = None
    try:
        server_port = int(input("server port:"))
    except:
        print("Error! The port must be integer!")
        exit()
    server_addr = (server_ip, server_port)
    tcp_socket.connect(server_addr)
    print("connect has created")
    return tcp_socket

def send_data(tcp_socket):
    # 3. 发送数据/接收数据
    send_data = input(">")
    tcp_socket.send(send_data.encode("utf-8"))  # udp用sendto方法(因为没有链接,所以每次发送都必须知道目的地),
    # tcp因为有链接,所以使用send方法即可
    if send_data == "exit()":
        exit()

def recv_data(tcp_socket):
    recv_msg = tcp_socket.recv(1024)
    print("received msg from server: {}".format(recv_msg.decode("utf-8")))

def main():
    tcp_socket = creat_link()
    while True:
        send_data(tcp_socket)
        recv_data(tcp_socket)

    #4. 关闭tcp套接字
    tcp_socket.close()

if __name__ == "__main__":
    main()

服务器端:
tcp服务器的流程如下(以买手机类比):

  • socket创建一个套接字(买了手机)
  • bind绑定ip和port(插上手机卡)
  • listen使套接字变为可以被动链接(设置手机为正常接听状态——即能够响铃)
  • accept等待客户端的链接(等待别人拨打)
  • recv/send接收或发送数据(进行通话)
#服务器端必须绑定 
from socket import *

def creat_server():
    # 1. 买个手机(创建套接字)
    tcp_server_socket = socket(AF_INET, SOCK_STREAM)
    # 2. 插入手机卡(绑定本地信息 bind)
    server_port = None
    try:
        server_port = int(input("set server port:"))
    except:
        print("port must be integer!")
        exit()
    tcp_server_socket.bind(("", server_port))
    # 3. 将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
    print("waiting connect...")
    tcp_server_socket.listen(128)
    return tcp_server_socket

def creat_link(tcp_server_socket):
    # 4. 等待别人的电话到来(等待客户端的链接 accept)
    new_client_socket, client_addr = tcp_server_socket.accept()  # accept的返回值是一个元组(client_socket, client_addr)
    print(str(client_addr) + " has connected")
    # 监听套接字默认会阻塞,直到有返回值
    # a, b = (11, 22)类似这样的形式被称之为拆包,前后数量必须对应
    # 通常是由一个套接字见监听,然后每accept到一个链接请求,就创建一个新的套接字与其链接(为客户端服务)
    # 通常是服务器先接收,客户端先发送
    return (new_client_socket, client_addr)

def recv_and_return_msg(new_client_socket, client_addr):
    recv_data = new_client_socket.recv(1024)    #如果客户端已经close(),则recv也会解阻塞,此时recv_data为空,
    #如果是因为客户端发送了数据导致recv解阻塞,则recv_data不为空
    #ps:客户端无法发送空内容
    if recv_data.decode("utf-8") == "exit()":
        return "exit()"
    print(str(client_addr) + ":", end="")
    print(recv_data.decode("utf-8"))
    new_client_socket.send(recv_data)

def close_all_socket(tcp_server_socket, new_client_socket):
    # 关闭套接字
    tcp_server_socket.close()
    new_client_socket.close()
    print("Link has be closed")

def main():
    tcp_server_socket = creat_server()
    new_client_socket, client_addr = creat_link(tcp_server_socket)

    while True:
        tag = recv_and_return_msg(new_client_socket,client_addr)
        if tag == "exit()":
            close_all_socket(tcp_server_socket, new_client_socket)
            exit()

if __name__ == "__main__":
    main()

案例:文件下载器

客户端:

from socket import *

def main():
    #1. 创建套接字
    tcp_socket = socket(AF_INET, SOCK_STREAM)
    #2. 获取服务器的ip, port
    dest_ip = input("server ip:")
    dest_port = int(input("server port:"))
    #3. 链接服务器
    tcp_socket.connect((dest_ip, dest_port))
    #4. 获取下载文件的名字
    file_name = input("请输入要下载的文件名字:")

    #5. 将文件名字发送给服务器
    tcp_socket.send(file_name.encode("utf-8"))
    #6. 接收文件中的数据
    recv_data = tcp_socket.recv(1024*1024)  #为了使程序简化,假设文件最大1M
    #7. 保存接收到的数据到文件中
    with open("[副件-]" + file_name, "wb") as f:
        f.write(recv_data)
        print("文件:[{}]下载完成!".format(file_name))
    #8. 关闭套接字
    tcp_socket.close()
if __name__ == "__main__":
    main()

服务器端:

from socket import *

def creat_server():
    # 1. 买个手机(创建套接字)
    tcp_server_socket = socket(AF_INET, SOCK_STREAM)
    # 2. 插入手机卡(绑定本地信息 bind)
    server_port = None
    try:
        server_port = int(input("set server port:"))
    except:
        print("port must be integer!")
        exit()
    tcp_server_socket.bind(("", server_port))
    # 3. 将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
    print("waiting connect...")
    tcp_server_socket.listen(128)   #listen的参数一般写128,和同一时刻服务器端建立的连接数有关系,但是主要由操作系统决定,并不是说写128就是最多128个连接
    return tcp_server_socket

def creat_link(tcp_server_socket):
    # 4. 等待别人的电话到来(等待客户端的链接 accept)
    new_client_socket, client_addr = tcp_server_socket.accept()  # accept的返回值是一个元组(client_socket, client_addr)
    print(str(client_addr) + " has connected")
    # 监听套接字默认会阻塞,直到有返回值
    # a, b = (11, 22)类似这样的形式被称之为拆包,前后数量必须对应
    # 通常是由一个套接字见监听,然后每accept到一个链接请求,就创建一个新的套接字与其链接(为客户端服务)

    # 通常是服务器先接收,客户端先发送
    return (new_client_socket, client_addr)

def recv_and_return_msg(new_client_socket, client_addr):
    file_name = new_client_socket.recv(1024)
    if file_name.decode("utf-8") == "exit()":
        return "exit()"
    print(str(client_addr) + ":", end="")
    print("正在发送文件:" + file_name.decode("utf-8"))
    with open(file_name,"r") as f:
        content = f.read()
        new_client_socket.send(content.encode("utf-8"))

def close_all_socket(tcp_server_socket, new_client_socket):
    # 关闭套接字
    tcp_server_socket.close()
    new_client_socket.close()
    print("Link has be closed")

def main():
    tcp_server_socket = creat_server()
    new_client_socket, client_addr = creat_link(tcp_server_socket)
    recv_and_return_msg(new_client_socket,client_addr)
    close_all_socket(tcp_server_socket, new_client_socket)

if __name__ == "__main__":
    main()

结合多线程实现群聊

客户端:

from socket import *
import time
import logging as logger
import concurrent.futures as futures

logger.basicConfig(level=logger.DEBUG)

class Tcp_Client:
    def __init__(self, host = "127.0.0.1", port = 9999):
        self.sock = socket(AF_INET, SOCK_STREAM)
        self.sock.connect((host, port))

    def send(self, msg):
        """
        向服务器发送信息
        :param msg:
        :return:
        """
        self.sock.send(msg.encode("utf-8"))

    def read(self):
        """
        :return:
        """
        while True:
            data = self.sock.recv(1024)
            print(data.decode("utf-8"))


def main():
    ex = futures.ThreadPoolExecutor(max_workers = 5)
    tc = Tcp_Client()
    ex.submit(tc.read)

    while True:
        data = input(">")
        tc.send(data)


main()

服务器端:

from socket import *
import logging as logger
import concurrent.futures as futures

logger.basicConfig(level=logger.DEBUG)

class TcpServer:
    def __init__(self, port = 9999):
        self.host = ""  #由于是服务器端,所以host为空
        self.port = port
        self.clients = []
        self.ex = futures.ThreadPoolExecutor(max_workers = 5)

    def run(self):
        logger.info("服务器启动,监听端口:%d" % self.port)
        self.server_sock = socket(AF_INET, SOCK_STREAM)
        self.server_sock.bind((self.host, self.port))
        self.server_sock.listen(5)
        while True:
            client_socket, client_addr = self.server_sock.accept()
            self.ex.submit(self.read, client_socket)
            logger.info("客户端{}:{}建立连接".format(client_addr[0],str(client_addr[1])))
            self.clients.append((client_socket, client_addr))

    def read(self, client_socket):
        while True:
            data = client_socket.recv(1024)
            print(data.decode("utf-8"))
            for client in self.clients:
                sock = client[0]
                addr = client[1]
                if sock != client_socket:
                    info = addr[0] + ":"  + str(addr[1]) + ":" + data.decode("utf-8")
                    logger.info("向客户端{}:{}发送信息".format(addr[0], str(addr[1])))

                    sock.send(info.encode("utf-8"))


TcpServer().run()

TCP与UDP的流程对比

udp:                    tcp客户端:         tcp服务器端:
socket                  socket              socket
bind                    connect             bind
recvfrom/sendto         send/recv           listen
close                   close               accept
                                            recv/send
                                            close
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容