《老鸟python 系列》视频上线了,全网稀缺资源,涵盖python人工智能教程,爬虫教程,web教程,数据分析教程以及界面库和服务器教程,以及各个方向的主流实用项目,手把手带你从零开始进阶高手之路!点击 链接 查看详情




TCP编程

阅读:294123659    分享到

两台计算机相互通信,就需要遵守规定的协议,就如人相互之间做生意要遵守约定的合同一样,早期的计算机网络,都是由各厂商自己规定一套协议,各大公司如 IBM,Apple 和 Microsoft 都有各自的网络协议,互不兼容,如果两台计算机使用不同的协议就会导致无法通信,就如两个人约定的合同不一样就无法做生意一样。

为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,就如我们都遵守世贸协定,各个国家之间做生意就有章可循了。计算机之间最流行的通信协议为 TCP 和 UDP 协议。

目前世界上的操作系统基本上都实现了 TCP 和 UDP 协议,我们在编写程序的时候,用套接字直接使用操作系统提供的 API 即可。

套接字就是网络通信协议其中一种最常见的现实方式,套接字也叫 socket,它是基于 C/S 架构的,也就是说进行 socket 网络编程,通常需要编写两个程序,一个服务端,一个客户端。

我们基于 TCP 协议,写两个应用程序进行通信,一个叫做服务器,一个叫做客户端。客户端就如打电话的一端,服务器类似于被呼叫的一端。

服务器

首先要创建套接字,绑定套接字到本地 IP 与端口,然后开始监听连接,等待客户端的连接请求,接收传来的数据,或者发送数据给对方,传输完毕后,关闭套接字。

import socket

sv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建套接字
sv_ipport = ("0.0.0.0", 8889)          # 监听的 ip 和端口
sv_socket.bind(sv_ipport)              # 绑定服务地址
sv_socket.listen(5)                    # 协议栈缓冲区最大套接字存放个数
print('启动服务器,等待客户端连接......')

server_socket, addr = sv_socket.accept()
while True:
    client_data = server_socket.recv(1024).decode("utf-8")  # 接收信息
    if (client_data == "exit") or (not client_data):        # 判断客户端是否申请结束会话或客户端是否退出
        break
    print("收到客户端信息:%s" % client_data)
    server_data = input("请输入要发送给客户端的信息:")
    server_socket.send(server_data.encode("utf-8"))         # 发送信息

server_socket.close()  # 关闭套接字

现在我们先简单分析一下服务器端的代码,下节课我们会详细讲解套接字的各种知识,大家这节课先把服务器和客户端跑起来,能正常通信就达到了学习的目的。

首先,创建一个基于 IPv4 和 TCP 协议的 Socket。其中 AF_INET 代表协议家族,AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型,AF_INET6 则是 IPv6 的;而 AF_UNIX 则是 Unix 系统本地通信,我们使用 AF_INET 的目的就是使用 IPv4 进行通信,因为 目前 IPv6 还没有普及。SOCK_STREAM 是指流式套接字,也就是基于 TCP 协议的 socket。

sv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

服务器进程的套接字首先要绑定一个 IP,我在此绑定的 IP 是 0.0.0.0,该 IP 代表本机的所有 IP 都可以,该 IP 代表本机回送 IP127.0.0.1 或者本机公网 IP 或者本机局域网 IP。然后服务器进程还需要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立 socket 连接,随后的通信就靠这个 socket 连接了。

如果我们把电脑看成一个小区,IP 地址就相当于一个小区,一个小区有很多户人家,端口就相当于一个小区的某户人家,两个进程通信,就是来访者到该小区的某户进行拜访交流。所以套接字一定要包含 IP 和端口才能通信。

服务器可以同时响应多个客户端的请求,就如某户人家可以同时接待很多朋友来访一样。但是,每个服务器对每个客户端的连接都需要并发处理,这样才不会让客户端感觉受到了怠慢,并发的方案有多进程多线程或者单线程异步轮询比如协程等等,否则,服务器一次就只能服务一个客户端了。

sv_socket.bind(('0.0.0.0', 8889))

listen 的参数代表的是 TCP/IP 协议栈缓冲区最大阻塞监听连接数,也就是可以存储的返回给 accept 的客户端的连接请求最大个数,这个参数是面试题经常问到的问题,很少有人能正确的回答,下节课我们会用案例来详细讲解这个参数。

sv_socket.listen(5)

关于 accept 的是什么意思,也是面试的时候经常问到的问题,accept 是服务器进程从操作系统的 TCP/IP 协议栈缓冲区中取出发起连接的客户端的套接字,这个套接字就是 listen 对接的客户端的套接字,当 accept 取出该套接字返回给应用层,协议栈就把该套接字去掉,listen 可处理的客户端连接就多出来一个。

server_socket, addr = sv_socket.accept()

很多人认为 recv 是接收客户端发来的字节流,这是错误的认知,实际上 recv 是从当前操作系统的 TCP/IP 协议栈缓冲区内取出网卡已经接收到的客户端发来的字节流。关于 recv 的案例我们下节课详细讲解。

注意:我们用 recv 接收到的数据是客户端发来的字节流(因为网络上只能传输字节流),所以我们用 decode("utf-8") 解码成字符串。

client_data = server_socket.recv(1024).decode("utf-8")

如果客户端调用 close 关闭套接字,我们服务器端就会收到客户端发来的空字节流(注意:这个空字节流是客户端的操作系统的 TCP 协议栈发送的),所以我们判断 client_data 为空时,我就认为客户端关闭连接。

我们脑补一下,双方通信就如一次会谈,有的客人想主动结束这次会谈,直接就走了(直接调用套接字的 close),但有些客人出于礼貌,他在走之前发给我们一个含蓄的信息,告诉我们他想走了,让我们主动结束这次会谈。对于代码 client_data == "exit",这个意思就是客户端告诉我们他想走了,我们也不强人所难,就调用了 break 退出这个循环。然后调用 server_socket.close()结束这次会谈。

if (client_data == "exit") or (not client_data)
    break

send 函数并不是把数据发送到对方机器上,而是发送数据到本机操作系统的 TCP/IP 协议栈缓冲区,然后由协议栈缓冲区通过网卡把数据发送到网络上,数据在网络上通过路由器找到客户端的 IP 地址,然后在通过端口号找到客户端的通信进程。

因为网络上只能传送字节流,我们首先用代码 encode("utf-8")要把发送的数据编码成字节流,然后调用 send 发送到本机的 TCP/IP 协议栈缓冲区。

server_socket.send(server_data.encode("utf-8"))

注意,在输入内容的时候,如果直接敲回车,input 会收到空字符串,但是,send 函数是不能发送空内容的(只有调用 close 的时候,操作系统的 TCP 协议栈才能发送空字节流),所以,send 就成了一条无效的语句。

客户端

我们在此写个和服务器通信的客户端代码,大家先运行上面的服务器代码,然后再运行这个客户端代码,双方就可以进行数据传输了,客户端代码如下。

import socket

client_socket = socket.socket()     # 创建套接字
ip_port = ("127.0.0.1", 8889)       # 要连接的服务器的 ip 和端口
client_socket.connect(ip_port)      # 连接服务器
while True:
    client_data = input("请输入要发送给服务器的信息:")
    client_socket.send(client_data.encode("utf-8"))         # 发送信息

    server_data = client_socket.recv(1024).decode("utf-8")  # 接收信息
    if (server_data == "exit") or (not server_data):        # 判断服务器是否申请结束会话或客户端是否退出
        break
    print("收到服务器信息:%s" % server_data)

client_socket.close()  # 关闭套接字

我们在此先简单分析一下客户端的代码,下节课我们会详细讲解客户端的几个经常被理解错误,面试经常被问到的几个函数,这节课先把服务器和客户端跑起来,能正常通信就达到了学习的目的。

首先,客户端应该知道通信的服务器端的 IP 和端口,注意,客户端不能用 "0.0.0.0"这个地址去连接服务器,如果客户端和服务器是在一台机器上,我们一般用 "127.0.0.1",如果是在同一个局域网的不同机器上,我们用服务器的局域网 IP 地址(192.168.X.X 或者10.X.X.X)开头的 IP 地址,如果不在同一局域网上,我们用服务器的公网 IP。当然,如果是同一台机器,我们可以使用 "127.0.0.1",也可以使用局域网 IP 或者公网 IP。

ip_port = ("127.0.0.1", 8889)       # 要连接的服务器的 ip 和端口

connect 函数向服务器端发起来三次握手,三次握手成功后,就和服务器建立了连接,关于连接这个概念,也是面试的时候经常要问的,老鸟面试了 N 多个程序员,能说出个所以然的码农很少,包括很多大厂的资深程序员,我在此简单说一下,详细分析放到下一节课,三次握手建立的连接是逻辑上的概念,也就是双方的操作系统的 TCP/IP 协议栈缓冲区的套接字状态设定为 ESTABLISHED 状态,这个值就是认为双方是连接上了(经过了三次握手的考验),哪怕你把双方的网线拔掉了,这两个进程也认为他俩还是连接着的。就如有个段子说的:“我们只认证,不认人”。

client_socket.connect(ip_port)      # 连接服务器

大家还记得服务器端的 listen 函数吗,我们这个客户端的 connect 成功后,服务器进程所在的计算机就会在他的操作系统的 TCP/IP 协议栈缓冲区中创建一个和客户端通信的套接字,这个套接字就占用了一个 listen 名额,被阻塞在 listen 队列中,直到服务器的 accept 函数从协议栈缓冲区取出这个套接字。

程序结果演示

首先我们先运行服务器,然后运行客户端,在客户端中输入“hello”,然后敲回车,这时候我们就看到服务器程序打印出了“收到客户端信息:hello”。然后服务器输入“你好,老鸟python”,这时候我们看到客户端程序打印出“收到服务器信息:你好,老鸟python”。我们在客户端输入“exit”,然后我们发现服务器进程结束,客户端进程也结束了。

注意:同一种协议类型的端口一旦被占用,就不能再被其它进程使用了,比如我们服务器绑定了基于 TCP 协议的端口 8889,其它基于 TCP 协议的进程如果再绑定 8889 端口,就会失败。如果是基于其它协议类型,比如 UDP,则可以使用 8889端口,这个端口和 TCP 的 8889 端口不是一个。当然,UDP 的端口也不能被同一种协议重复绑定。

本节重要知识点

弄明白服务器和客户端代码整个流程。

能跑通程序实现通信。

作业

把服务器和客户端代码重新写一遍,让程序可以通信。


如果以上内容对您有帮助,请老板用微信扫一下赞赏码,赞赏后加微信号 birdpython 领取免费视频。


登录后评论

user_image
Ooho
2020年10月15日 20:16 回复

据说TCP传输不存在丢包,失真。请问它是如何实现的呢?对于应用层来说,我发送的数据,对方一定能收到,底层应该增加了校验和重发机制。


user_image
老鸟python
2020年11月30日 06:31

有啊,滑动窗口和选择重传,用ACK来确认是否收到。TCP分组头部有同步和纠错信息。


user_image
写作之欲正在高涨
2020年10月14日 12:00 回复

谢谢,对于初学者的我来说感到受益匪浅!


user_image
青芒果0527
2020年9月22日 06:49 回复

生动形象,点赞


user_image
InfoQ中文站兼职编辑
2020年5月18日 16:15 回复

相见恨晚


user_image
Dustin
2019年10月19日 16:25 回复

这是我见过最通俗易懂的教程


user_image
lookas2001
2019年10月10日 07:22 回复

写的真心不错,学习方法很好


user_image
陈皓
2019年8月29日 00:54 回复

写得那么好,真可惜不能让更多人看到。


user_image
莴补牛批
2019年5月21日 15:37 回复

可以很多客户端同时连接一个服务端吗


user_image
phodal-InfoQ社区编辑
2019年4月21日 19:45 回复

佩服 写的真心好 赞一个


user_image
王保平
2019年1月31日 03:16 回复

好的很


user_image
JeAnine9ann
2018年11月22日 07:14 回复

写的真心好+1 少赞太可惜了