# TCP和UDP

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

  • 简而言之

    其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
    也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。
    所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
    
  • 套接字得发展史: (套接字分为两类,文件类型和网络类型)

    • (文件)套接字家族的名字:AF_UNIX(起源于UNIX)

      unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

    • (网络)套接字家族的名字:AF_INET

      (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

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

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

  • 具体的三次握手,四次挥手过程:

    • 三次握手

    • 第一次握手:主机A发送位码为syn=1,随机产生seq number=x 的数据包(以便验证服务端是否收到,收到会返回加一数)到服务器(B),客户端进入SYN_SEND状态,等待服务器(B)的确认;主机B由SYN=1知道,A要求建立联机;

    • 第二次握手:主机B收到请求后要确认联机信息,向A发送ack number(主机A的seq+1),syn=1,ack=x+1,随机产生seq=y的包,此时服务器(B)进入SYN_RECV状态;

    • 第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手

    • 问题一为什么连接建立需要三次握手,而不是两次握手?

      • 答:防止失效的连接请求报文段被服务端接收,从而产生错误,也就是说要确认客户端还活着。若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入ESTABLISHED状态,而服务端在收到连接请求后就进入ESTABLISHED状态。此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源。
    • 四次挥手

    • 第一次挥手:主机1(可以使客户端,也可以是服务器端),设置Sequence NumberAcknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;

    • 第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment NumberSequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我也没有数据要发送了,可以进行关闭连接了;

    • 第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入CLOSE_WAIT状态;

    • 第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了

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

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

    • **问题二:client发送完最后一个ack之后,进入time_wait状态,但是他怎么知道server有没有收到这个ack呢?莫非sever也要等待一段时间,如果收到了这个ack就close,如果没有收到就再发一个fin给client?这么说server最后也有一个time_wait哦?**求解答

      • 答:因为网络原因,主动关闭的一方发送的这个ACK包很可能延迟,从而触发被动连接一方重传FIN包。极端情况下,这一去一回,就是两倍的MSL时长。如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动

        关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:1.旧的TCP连接已经不存在了,系统此时只能返回RST包2.新的TCP连接被建立起来了,延迟包可能干扰新的连接,这就是为什么time_wait需要等待2MSL时长的原因。

    • 问题三:为什么要四次挥手?

      • 答:确保数据能够完整传输。

        ​ 当被动方收到主动方的FIN报文通知时,它仅仅表示主动方没有数据再发送给被动方了。但未必被动方所有的数据都完整的发送给了主动方,所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后,再发送FIN报文给主动方,告诉主动方同意关闭连接,所以这里的ACK报文和FIN报文多数情况下都是分开发送的

总结:
TCP协议:
   1、 面向连接\可靠\慢\对传递的数据的长短没有要求
   2、 两台机器之间要想传递信息必须先建立连接
   3、 之后在有了连接的基础上,进行信息的传递
   4、可靠 : 数据不会丢失 不会重复被接收
   5、慢 : 每一次发送的数据还要等待结果
   6、 三次握手和四次挥手	
UDP协议:
    1、无连接\不可靠\快\不能传输过长的数据0
    2、机器之间传递信息不需要建立连接 直接发就行
    3、不可靠 : 数据有可能丢失
    
五层协议:
    1、应用层       python   send(b'hello,world')
               # socket(虚拟,对下面的进行了整合,直接使用
    2、传输层      端口 tcp/udp协议     四层路由器  四层交换机
    3、网络层      ip地址相关 ip协议      路由器    三层交换机
    4、数据链路层   mac地址相关 arp协议   网卡      二层交换机
    5、物理层       网线

# 套接字的使用 (有收必有发,收发必相等)

  • 基于TCP协议的socket使用:

    #基础服务端   需要(s、b、l、a) 就可以send()、recv()
    
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
    sk.listen()               #监听链接
    conn,addr = sk.accept()   #接受客户端链接
    ret = conn.recv(1024)     #接收客户端信息
    print(ret)               #打印客户端信息,需要时要解码decode
    conn.send(b'hi')         #向客户端发送信息,编码必要时
    conn.close()             #关闭客户端套接字,不是closed
    sk.close()               #关闭服务器套接字(可选)
    
    #基础客服端  只需要(s、c)才可以send()、recv()
    import socket
    sk = socket.socket()           # 创建客户套接字
    sk.connect(('127.0.0.1',8898))    # 尝试连接服务器,0,0,0,1是全网段,所有来防本机的都可以
    sk.send(b'hello!')
    ret = sk.recv(1024)         # 对话(发送/接收)
    print(ret)
    sk.close()                   # 关闭客户套接字
    
    #注意,在服务端启动时,如果遇到,Address alreadly in use,则需要在bind前加sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 重用端口
    
    #TCP的聊天版本服务端:可多连接,但是必须一对一回复
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',9001))
    sk.listen()
    while True:
        conn,addr = sk.accept()   # 等待用户来连接我
        while  True:
            msg = input('>>>')
            conn.send(msg.encode('utf-8'))
            if msg.upper() == 'Q':
                break
            content = conn.recv(1024).decode('utf-8')  # 等待 客户端给我φ消息
            if content.upper() == 'Q': break
            print(content)
        conn.close()
    sk.close()
    
    #TCP客户端:
    import socket
    sk = socket.socket()
    sk.connect(('127.0.0.1',9001))
    while True:
        ret = sk.recv(1024).decode('utf-8')
        if ret.upper() == 'Q':break
        print(ret)
        msg = input('>>>')
        sk.send(msg.encode('utf-8'))
        if msg.upper() == 'Q':
            break
    
    sk.close()
    
  • 基于UDP协议的socket的使用:(客服端和服务端都要用type=socket.SOCK_DGRAM,且客服端的sendto(内容,地址),没有connect)

    #基础服务端  只需要(s、b)就可以recvfrom()接收地址和消息,再sendto(msg,addr)
    import socket
    udp_sk= socket.socket(type=socket.SOCK_DGRAM)  #创建一个服务器的套接字
    udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
    msg,addr = udp_sk.recvfrom(1024)    # 如果有from则需要addr接收地址
    print(msg)
    udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
    udp_sk.close() 
    
    #基础客服端  只需要sk、sendto(msg,add)
    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)
    
    #进阶服务端
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1',9001))
    while True:
        msg,client_addr = sk.recvfrom(1024)
        print(msg.decode('utf-8'))
        content = input('>>>')
        sk.sendto(content.encode('utf-8'),client_addr)
    sk.close()
    
    #进阶客户端
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    server_addr = ('127.0.0.1',9001)
    while True:
        content = input('>>>')
        if content.upper() == 'Q':break
        sk.sendto(content.encode('utf-8'),server_addr)
        msg = sk.recv(1024).decode('utf-8')
        if msg.upper() == 'Q':break
        print(msg)
    sk.close()
    
    #高级QQ服务端(一对多)
    import json
    import socket
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1',9001))
    user_info = {
        1234:('alex','\033[1;32m'),
        5678:('宝元','\033[1;31m'),
    }
    while True:
        msg,addr = sk.recvfrom(1024)
        str_msg = msg.decode('utf-8')
        dic_msg = json.loads(str_msg)
        code = dic_msg['code']
        content = dic_msg['msg']
        print('%s%s : %s\033[0m'%(user_info[code][1],user_info[code][0],content))
        ret = input('>>>').encode('utf-8')
        sk.sendto(ret,addr)
    sk.close()
    
    #客服端
    import json
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)
    addr = ('127.0.0.1',9001)
    code = 1234
    while True:
        msg = input('>>>')
        if msg.upper() == 'Q':break
        send_msg = {'code':code,'msg':msg}
        str_msg = json.dumps(send_msg)
        sk.sendto(str_msg.encode('utf-8'),addr)
        ret = sk.recv(1024).decode('utf-8')
        if ret.upper() == 'Q':break
        print(ret)
    sk.close()
    
    
    #客服端远程登陆服务端(服务端,这里需要说明的是往往密码都是现在客服端加密后,再在服务端加盐,最后存入数据库
    import sys
    import json
    import socket
    import hashlib
    
    def get_md5(username,password):
        md5 = hashlib.md5(username.encode('utf-8'))
        md5.update(password.encode('utf-8'))
        return md5.hexdigest()
    
    def login(dic_msg):
        print(dic_msg['user'], dic_msg['pwd'])
        with open('userinfo', encoding='utf-8') as f:
            for line in f:
                user, pwd = line.strip().split('|')
                print(pwd,get_md5(dic_msg['user'], dic_msg['pwd']))
                if user == dic_msg['user'] and pwd == get_md5(dic_msg['user'], dic_msg['pwd']):
                    return {'opt':'login','result':True}
            else:return {'opt':'login','result':False}
    
    sk= socket.socket()
    sk.bind(('127.0.0.1',9001))
    sk.listen()
    while True:
        conn,addr = sk.accept()
        msg = conn.recv(1024).decode('utf-8')
        dic_msg = json.loads(msg)
        if hasattr(sys.modules[__name__],dic_msg['operate']):
            ret = getattr(sys.modules[__name__],dic_msg['operate'])(dic_msg)
            content = json.dumps(ret).encode('utf-8')
            conn.send(content)
        conn.close()
    sk.close()
    
    #客服端
    import json
    import socket
    import hashlib
    
    def get_md5(username,password):
        md5 = hashlib.md5(username[::2].encode('utf-8'))
        md5.update(password.encode('utf-8'))
        return md5.hexdigest()
    username = input('用户名 :')
    password = input('密  码 :')
    sk = socket.socket()
    sk.connect(('127.0.0.1',9001))
    msg = {'operate':'login',
           'user':username,
           'pwd':get_md5(username,password)}
    str_msg = json.dumps(msg)
    sk.send(str_msg.encode('utf-8'))
    content = sk.recv(1024)
    str_content = content.decode('utf-8')
    dic_content = json.loads(str_content)
    if dic_content['result']:
        print('登录成功')
    else:
        print('登录失败')
    sk.close()
    

# socket的相关方法

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据,发送一个字符串套接字,知道所有都发完,递归版的send
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          返回套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

#设置阻塞
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.setblocking(False)   # blocking阻塞  setblocking设置阻塞
sk.listen()

conn_l = []
while True:
    try:
        conn,addr = sk.accept()
        conn_l.append(conn)
    except BlockingIOError:
        for conn in conn_l:
            try:
                msg = conn.recv(1024).decode('utf-8')
                print(msg)
                conn.send(msg.upper().encode('utf-8'))
            except BlockingIOError:
                pass

# 网络基础总结

  1. 集线器(HUB): HUB是一个多端口的转发器.主要功能是对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上.将咱们所有的插上集线器的电脑连通起来.
  2. 交换机(Switch): 升级版集线器.它可以为接入交换机的任意两个网络节点提供独享的电信号通路.
  3. 网卡: 作用是接收电信号,有网络接口(用于插网线) 网卡是局域网中连接计算机和传输介质的接口,能实现与局域网传输介质之间的物理连接和电信号匹配.
  4. MAC地址(物理地址): MAC地址又称为物理地址,硬件地址,用来定义网络设备的位置.     MAC地址是网卡决定的,固定的,全球唯一的,相当于每台电脑的上网身份证.   物理地址: ‎20-47-47-68-EE-DF->16进制的6个数表示,前三位厂商编号,后三位生产流水号
  5. 多播\单播: 多播指一点对多点的通信,在IPv6中把广播看作是多播的一个特殊例子. 单播指客户端与服务器之间的点到点连接.
  6. 广播风暴: 缺点是 -->不安全,容易拥堵网络.
  7. IP地址: 英文是Internet Protocol Address, 指互联网协议地址. IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异.    作用: 划分广播域        分类:       IPv4: 4个点份十进制 (目前使用较多)       IPv6: 6个冒号分十六进制
  8. DHCP协议: 两个作用    (1)给内部网络或网络服务供应商自动分配IP地址.  (2)给用户或者内部网络管理员作为对所有计算机作中央管理的手段.
  9. 子网掩码: 计算是否属于同一网段,属于同一网段的,我们称为属于同一子网.子网掩码不能单独存在,它必须结合IP地址一起使用.子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分.
  10. DNS服务器: 具有域名解析的作用,即把域名解析为IP地址. 默认跟着网关走. 记录着所有的域名和它网站对应的那台服务器的IP地址的对应关系,理解为一个字典.
  11. 路由器: 是连接因特网中各局域网,广域网的设备,它会根据信道的情况自动选择和设定路由,以最优路径,按前后顺序发送信号.
  12. 路由协议: 用于确定到达路径,计算最优路径.
  13. 网关: 是一个网络连接到另一个网络的"关口",也就是网络关卡. 它是公网IP,也可以说是路由器的IP地址.
  14. NAT技术: 网络地址转换,将局域网的IP地址转换为公网(网关)的IP地址
  15. 局域网: 也称内网
  16. 端口:标识电脑上某个应用程序. 通过IP地址+端口-->我们就能唯一确定一台电脑上的某个应用程序.

如果把IP地址比作一间房子,端口就是出入这间房子的门.真正的房子只有几个门,但是一个IP地址的端口可以有65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从 0 到 65535(2^16-1). 其中0-1024是电脑内部服务使用的,不要去占用. 一般来说,自己编程时不去占用8000以内的数值.

  1. 端口映射: 访问网站时自动携带一个端口号,用于直接访问服务端

# socket解读

  • sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)

    参数一:地址簇
      socket.AF_INET IPv4(默认)
      socket.AF_INET6 IPv6
      socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
    
    参数二:类型
      socket.SOCK_STREAM  流式socket , for TCP (默认)
      socket.SOCK_DGRAM   数据报式socket , for UDP
    
      socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
      socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
      socket.SOCK_SEQPACKET 可靠的连续数据包服务
    
    参数三:协议
      0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的