套接字编程——创建网络应用

参考 计算机网络——自顶向下

套接字编程

到现在为止,我们已经了解了一些重要的网络应用,接下来,让我们一起来探索网络应用程序实际上是如何创建的?
回忆一下,我们在2.1节讲过,一个typical的网络应用包括一对程序:客户端程序和服务器端程序,这两个程序分别存储在两个不同的端系统中。
当这两个程序执行的时候,一个客户端进程和一个服务器端进程就被创建了,那么位于两个主机上的进程是如何通信的呢?
这两个进程是通过从他们的套接字中读取或者写入数据来通信的。
你可以把套接字想象成自己的手机,把进程想象成你自己。地球的我和月球的我如何进行通信呢?地球上的我通过写入数据给我地球上的手机,然后月球上的我通过月球上的手机查看数据。这个套接字好比就是我们的手机。想要跟我联系,直接往我的套接字里写入数据就可以了,我自然会在一段时间之后收到。

当我们在创建一个网络应用程序的时候,coder的主要任务就是为客户端程序和服务器端程序写代码。

有两种类型的网络应用。

我们根据什么对网络应用进行分类呀?就稀里糊涂冒出来两种应用,写书的人真的是服了。
余成林:根据是客户端和服务器是否均实现的是已经由RFC或者其他组织公开发表的协议

1.类型A
应用的源代码实现的都是协议标准中规定好的操作。
由于应用的行为的规则是众所周知的。

说实话,说了这么半天,还是不知道他在说什么
余成林:代码就是用来实现协议的,你要想使用协议提供的服务,你就需要按照协议提供的规则来写代码。例如你都邮局寄信,你得买信封,你得把地址写好等诸如此类的东西,这就叫做: 我们在实现协议。

举个例子,A类型应用的客户端程序可以是FTP协议客户端的一种实现,A类型应用的服务器端程序可以是FTP协议服务器端协议的实现。如果程序员Ben负责编写A类型应用的客户端程序, 程序员Amy为服务器端程序写代码,并且Ben和Amy都仔细遵循了RFC的规则,那么这两个程序一定是可以协同运行的。

今天的许多网络应用都涉及到独立开发者所创建的客户端程序和服务器端程序的通信。
例如:火狐浏览器和Apache Web服务器,显然客户端程序(火狐浏览器)和 服务器端程序(Apache Web服务器)是由两个不同的公司开发的,但是由于他们遵循的协议都是HTTP,所以他们可以协同工作。

哦,这句话算是把上面讲清楚了。
余成林:加油!

2.类型B
专用网络应用程序(proprietary(专卖的) network application) )
在这种专用网络应用中,客户端程序和服务器端程序使用的应用层协议是还没有被RFC或者其他权威机构公开发表的。一个独立的开发团队把服务器端程序和客户端程序的编码全包了。

开发团队:这点代码小case
余成林:可以的

但是因为这个团队写的代码没有实现一个公开发表的协议,所以其他的独立开发者不能够开发出和这个应用能够协同工作的代码。

余成林:我连他们的规则都不知道,我怎么开发!
余成林:类型A就是客户端和服务器端都是遵循某个公开协议实现的应用,类型B就是自己写的一些服务器应用,服务器客户端都自己一个人(或者一个团队包了),客户端和服务器端遵循的协议没有公开发表过,就他们自己一帮人在哪儿自己搞自己的。

在这一节,我们将会观察到在开发服务器-客户端应用程序的过程中的一些关键问题。

好了,关键问题是啥,快说
余成林: 都在后面的博文里

我们将会写代码,实现一个非常简单的客户端-服务器应用。

这个应用到底是属于类型A还是类型B?
余成林: 基于类型B,因为传输层协议是基于TCP或者UDP,这些协议都是已经公开的协议。

在开发这个应用的过程中,第一步,我们就必须要确定这个网络应用到底是run over TCP还是over UDP。

对,我们要要选好传输层协议。是用TCP还是用UDP!

记得我们在前面讲:

  • TCP是以连接为导向的,提供了一个可靠的字节流channel,数据通过这个channel在两个端系统之间流动。
  • UDP是无连接的,从一个端系统向另一个端系统发送独立的数据分组,不打任何保票。

记得我们还在前面讲,当一个客户端程序或者一个服务器端程序实现了由RFC定义的协议时,这个程序应该使用和这个协议所关联的well-known的端口号。

余成林: 从这句话可以看出,端口号其实是和协议相关联的. 但是协议是和应用相关联的,所以端口号是和应用相关联的,但是应用执行之后就称为了进程,所以端口号是和进程相关联的,所以端口号归根结底是和进程相关联的。

相反地,当我们开发一个专卖应用时(类型B应用),开发者应该避免使用那些well-known的端口号,端口号这个概念在2.1节讨论过,端口号还会在第3章详细讲)

余成林:确实讲解了,在2.1节的进程寻址那个地方讲解的。接收进程需要一个地址。为了标识该接收进程,需要定义两种信息:1.主机的地址 2.定义在目的主机中的接收进程的标识符。在因特网中,主机由其IP地址标识,除了知道报文发送目的地的主机地址外,发送进程还必须指定运行在接收主机上的接受进程(更具体的说,接收套接字),因为一般而言一台主机上能够运行许多网络应用,目的地端口号是就是为了标识进程的

我们通过一个简单的UDP应用和一个简单的TCP应用来介绍一下UDP和TCP套接字编程。

我们使用python语言来编写UDP应用和TCP应用。

当然,我们也可以使用Java ,C/C++来写代码,但是因为Python清晰地展示了一些关键的套接字概念,我们最终决定还是使用python写,这样才能够把概念给大家讲清楚呀!

2.7.1 UDP套接字编程

In this subsection, 我们将写一个使用UDP的客户端-服务器程序。

在下一节,我们会写一个使用TCP的客户端-服务器程序。

这典型的写书人的风格,在每一节的开始介绍一下这一节要做什么

我们在2.1节讲过,运行在不同机器上的进程彼此之间通过将报文发送到套接字中来进行通讯。

对,确实讲解了,刚复习的。

我们说过,进程就像一个房子,进程的套接字就像这个房子的门。

应用住在房子的门的一侧,传输层协议住在门的另一侧(外面的世界),如下图所示(余成林自己画的)。
在这里插入图片描述
应用开发者可以控制套接字在应用层一侧的一切,然而,应用开发者对传输层一侧几乎没有控制。

我到底可以控制哪些东西?
余成林:可以控制使用的传输层协议,套接字的端口号等
我不能控制哪些东西?
余成林:目的主机的IP地址,是多少就是多少。例如localhost,就是localhost,不能写成其他的。

书,其实要慢慢的啃,才有收获。但是一定要在推进。

现在,我们来仔细看一看两个使用UDP套接字的进程之间是如何通信的。

首先引入几个概念:
发送进程:发送数据的进程
接收进程:接收数据的进程
目的地地址:包括目的主机地址和目的进程端口号

在发送进程可以将一个分组推到套接字门外面之前,发送进程必须要给这个分组附上一个目的地地址。在这个分组经过发送进程的套接字门之后,因特网会使用这个目的地地址将这个分组引导到接收进程的套接字门。

当这个分组到达接收进程的套接字时,接收进程会通过这个套接字获取这个分组,然后检查这个分组的内容并采取合适的动作。

这真的像是中国人写的话。什么合适的动作?反正是合适的动作哈哈。
余成林:合适的动作后面再说

所以你现在可能在想,附加给这个分组的目的地地址到底是什么?

余成林:目的主机IP地址 + 目的进程端口号

正如你所期待的,目的主机的IP地址是目的地地址的一部分。

哦,原来目的主机的IP地址只是目的地址的一部分,可还行

通过将目的地IP地址包含在这个分组里,网络上的路由器就能够将这个分组导向到目的地主机。

但是因为一个主机上可能正在运行许多网络应用,每一个网络应用有一个或者多个套接字

这我就搞不懂了,怎么一个应用还会有多个套接字?
余成林:例如,使用TCP的应用有欢迎套接字和连接套接字,连接套接字可以有很多,很多。

因此有必要在目的地主机上标识某个具体的套接字。

用什么标识套接字?

对,没错就是端口号
当一个套接字被创建的时候,这个套接字就被赋予了一个端口号,这个端口号就可以标识这个套接字。

所以,正如你所期待的,这个分组的目的地地址也包含了套接字的端口号。

总结一下,发送进程给分组附上了一个目的地址,这个目的地址由

  • 目的主机的IP地址
  • 目的套接字的端口号

组成。

并且,我们马上就会看到,发送进程的源地址也被附加在了这个分组。

发送进程的源地址由以下两项组成:

  • 源主机的IP地址
  • 源套接字的端口号

然而,添加这个源地址给分组typically不是由UDP应用代码来做的,而是由下层的操作系统自动为我们做的。

也就是说,我们写程序的只管把目的地地址加到分组就好了。

我们将用下面这个简单的client-server应用来说明UDP和TCP套接字编程。

1.客户端从它的键盘读入一行字符,然后把这个数据发送给服务器
2.服务器收到数据,将这些字符转换成大写的。
3.服务器把修改后的数据发送给客户端
4.客户端收到修改的数据,并把数据展示在屏幕上。

Figure 2.28 高亮了客户端和服务器基于UDP服务通讯的主要socket-related的活动。

在这里插入图片描述

余成林:从UDP套接字发出来的都是包含有目的主机IP地址和目的进程端口号的UDP segment. 有个事情我要说一下,UDP是运输层协议,按照到运输层出来的都是报文segment, 但是由于UPD提供的服务过少,经常也被称为UDP datagram.

现在,让我们撸起袖子加油干!看一下这个客户端-服务器程序基于UDP的实现。

在每一个程序的后面,我们也提供了一个详细的,line-by-line的分析。

line-by-line可还行

我们从UDP客户端开始讲起。
UDP客户端发送一个简单的应用级别的报文给服务器。为了服务器能够收到并回复客户端的报文,服务器必须要准备好并且正在运行。也就是说,在客户端发送报文之前,服务器程序必须作为一个进程的形式正在运行。

客户端程序叫做 UDPClient.py, 服务器端程序叫做UDPServer.py.
为了突出一些关键问题,我们专门提供了尽可能简洁的代码。

可以,我喜欢。

在这个应用中,我们随机选择了一个服务器端口号是12000.

UDPClient.py

from socket import * # socket模块是python中所有网络通讯的基础,有了这个模块,我们就可以在我们的程序中创建套接字了。
serverName = 'hostName' # 服务器的IP地址或者是主机名。如果我们使用主机名,DNS查询会自动执行来获取IP地址。
serverPort = 12000 # 服务器端口号
clientSocket = socket(socket.AF_INET, socket.SOCK_DGRAM) # 创建了客户端的套接字,第一个参数socket.AF_INET表示网络使用的是IPv4(先不用管什么是IPv4,第4章会详细讲解) 第二个参数表示套接字的类型是socket.SOCK_DGRAM,意味着这个套接字是个UDP套接字,而不是TCP套接字。
# 这里记忆的方式可以是DGRAM, ---> datagram, UDP分组通常被称为datagram
# 注意,我们在创建客户端套接字时并没有指定客户端套接字的端口号,操作系统会为我们做。现在客户端进程的门已经创建完毕,我们现在想创建一条报文然后把这个报文通过这个门发送出去。
message = raw_input('Input lowercase sentence:') # raw_input是python的内置函数,表示从键盘中输入数据。
# 现在,我们有了套接字,有了报文,我们想把这个报文通过这个套接字发送给目的主机。
clientSocket.sendto(message, (serverName, serverPort))
# sendto方法将目的地址(serverName, serverPort)添加到了报文上,并将最终的分组发送到了发送进程的套接字。
# 我们在前面提到过,源地址也会附加到分组上,但是在代码里没有体现,操作系统帮我们做了。
# 通过UDP发送一个client-to-sever 报文很简单!
# 在发送这个报文之后,客户端就会等待收到来自服务器的数据。
# 当一个分组从因特网到达客户端的套接字之后,这个分组的数据就被放到了变量modifiedMessage, 并且这个分组的源地址被放到了变量serverAddress。 serverAddress包含了服务器的IP地址和服务器的端口号。
# UDPClient实际上并不需要这个服务器的地址信息,因为UDPClient从一开始就知道服务器的地址
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
# recvfrom方法将缓存大小设置为2048作为输入。
print modifiedMessage # 打印出修改的信息
clientSocket.close() # 关闭套接字,这个进程就终止了。

UDPServer.py

from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('',serverPort)) # 这一行表明端口12000被赋给了服务器的套接字。
# 因此,在UDPServer中,端口号是使用代码显式地绑定到了套接字。这样以来,当任何人发送分组给这个IP地址的服务器的端口12000时,这个分组就会被自动导向到这个套接字。
print "The server is read to receive"
while 1:
    # UDPServer等待分组到来
	message, clientAddress = serverSocket.recvfrom(2048)
	# UDPServer 会使用这个clientAddress作为它的返回地址
	modifiedMessage = message.upper()
	serverSocket.sendto(modifiedMessage, clientAddress)
	# 最后一行将客户端的地址添加到了报文,并把最终的结果分组发送到了这个服务器的套接字。在前面提到过,尽管我们没有在代码中明确指出,但是这个服务器的地址也被添加到了分组。

至此,我就把UDP套接字编程讲解清楚了。

TCP套接字编程

TCP与UDP不同, TCP是connection-oriented 协议。这意味着在客户端和服务器可以开始彼此发送数据之前,客户端和服务器需要握手并建立一个TCP连接。TCP连接的一端is attached to the client socket, TCP连接的另一端is attached to 服务器套接字。

当创建TCP连接时,我们将这个TCP连接和客户端套接字地址(IP地址和端口号)和服务器套接字地址(IP 地址和端口号)

TCP连接建立之后,当一方想要发送数据给另一方,发送方就通过发送方的socket将数据丢到TCP连接里就可以了。

这与UDP不同,对于UDP来说,服务器在讲一个分组丢到套接字之前必须要将目的地地址添加到分组。

余成林总结:TCP连接一旦建立,要发送数据,直接将分组丢到套接字。但是UDP在将分组丢到套接字之前,必须要将目的地址添加到分组。

现在,我们来仔细研究一下基于TCP的客户端和服务器程序的交互。

客户端有义务要先联系服务器,为了让服务器能够对客户端最初的联系做出反应,服务器必须处于ready的状态。这就意味着两件事情:

  1. 与UDP服务器一样,TCP服务器必须在客户端视图联系它之前就处于ready的状态。

就像现在保研,导师要想让学生联系它,首先要把自己的个人主页搭建好吧。发了哪些文章都亮一亮吧。

  1. 服务器程序必须有一个特殊的门——更准确的讲,一个特殊的套接字——这个套接字负责欢迎来自任意主机的客户端进程的联系。我们通过把客户端初次联系叫做“knocking on the welcoming door".

为啥服务器程序就必须要有一个特殊的门呢? 好像还是不是很理解,先放在这儿。
余成林:这个特殊的门是欢迎门。

我突然感觉这样读书效率才高,读着读着就会提出一些问题,然后把这些问题列出来,当把一章读完之后,要能够回过头来把最初的问题解决掉,说明你确实读懂了。哇!

当服务器进程正在运行时,客户端进程就可以initiate a TCP connection to the server.

客户端进程是如何initiate a TCP connection to the server?
余成林:客户端创建客户端TCP套接字,然后通过connect函数初始化。

客户端程序通过创建TCP套接字来initiate.

当客户端创建它的TCP套接字时, 客户单就指定了the address of the welcoming socket in the server,也就是服务器主机的IP地址和服务器套接字的端口号。

客户端创建完毕它的套接字之后,客户端initiates a three-way handshake and establishes a TCP connection with the server.
这个三次握手发生在传输层,客户端程序和服务器程序对三次握手是完全不可见的。

在三次握手期间,客户端进程首先knock on the welcoming door of the server process,。 当服务器"hears" the knocking, 服务器就创建了一个新的door------ 更准确地讲,专门针对这个特定的客户端的一个新的套接字。

在我们下面的例子中, the welcoming door is a TCP socket object that we call serverSocket; 新创建的套接字我们叫做connectionSocket.

那些第一次碰见TCP套接字的学生有时候会对the welcoming socket感到困惑。

我表示不是很困惑

每一个新创建的服务器端的connectionSocket专门就负责与某一个客户端的通信。

也就是说只要来一个客户端,就需要新创建一个连接套接字。

从应用的视角来看,客户端套接字和服务器的连接套接字是直接connected by a pipe.

如图2.29所示。
在这里插入图片描述
客户端进程可以发送任意字节到客户端套接字,TCP保证服务器进程会通过连接套接字一个字节不少的按序收到所有字节。

因此,TCP在客户端进程和服务器端进程之间提供了一个可靠的服务。

更进一步说,就像人们可以进出同一个门一样,客户端进程发送字节给它的套接字,同时也会从它的套接字中收到字节。

相似地,服务器进程不仅从它的连接套接字收到字节也发送字节到它的连接套接字。

我们同样用本文最开始举出的客户端-服务器应用来演示TCP套接字编程。

客户端发送一行数据给服务器,服务器将这一行数据变为大写,然后将这行数据返回给客户端。

Figure 2.30 高亮了主要的socket-related activity of the client and server that communicate over the TCP transport service.
在这里插入图片描述

图就是这么个图,看不懂就先不看了吧。

TCPClient.py

from socket import *
serverName = 'serverName'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_STREAM) # 第一个参数表明使用IPv4, 第二个参数表明这个套接字的类型是SOCK_STREAM, 这意味着它是一个TCP socket(rather than a UDP socket)
# 注意,我们再一次没有指定服务器套接字的端口号,我们还是让操作系统为我们做这件事情。
clientSocket.connect((serverName, serverPort))
# 回忆一下,在客户端可以向服务器发送数据之前(vice versa),客户端和服务器之间必须建立一个TCP连接,connect函数的参数是连接的服务器端的地址。在这一行代码执行完毕之后,三次握手就完毕,TCP连接就建立了!
sentence = raw_input('Input lowercase sentence:')
clientSocket.send(sentence) # 程序没有显示的创建分组,也没有将目的地址添加到分组中,而是直接将字符串drop into the TCP connection. 
# 然后客户端就等待从服务器接收分组。
modifiedSentence = clientSocket.recv(1024) 
print 'From Server:', modifiedSentence
clientSocket.close() # 关闭TCP连接,这将会导致客户端的TCP给服务器端的TCP发送一个TCP报文。这个我们会在3.5节讲,好吧!到时候一定要回来填这个坑。

为什么客户端在关闭TCP连接时,会向服务器端发送一个报文?3.5节见!

TCPServer.py

from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('',serverPort)) # 将服务器端口号和服务器套接字相关,但是对TCP俩说, serveSocket是我们的welcoming socket, 在建立the welcoming door之后,我们将要wait and listen for some client to knock on the door:
serverSocket.listen(1) # 这一行让服务器监听来自客户端的TCP连接请求,这个参数指定了排队连接的最大数目(至少是1)
print 'The server is ready to receive'
while 1:
	# 当a client knocks on this door, 这个程序就调用accept() method for serverSocket. 
	connectionSocket, addr = serverSocket.accept() # accept方法在服务器上创建了一个新的套接字connectionSocket,专门致力于这个特定的客户端。
# 然后这个客户端和服务器就完成握手,在客户端套接字和服务器的连接套接字之家安创建一个TCP连接,当这个TCP连接建立完成之后,客户端和服务器就可以彼此通过这个连接发送字节了。
	sentence = connectionSocket.recv(1024)
	capitalizedSentence = sentence.upper()
	connectionSocket.send(captializedSentence)
	connectionSocket.close()

在这个程序中,在发送完修改的句子给客户端之后,我们就关闭了connectionSocket。 但是由于serverSocket remains open ,another client 现在依然可以敲门,and send the server a sentence to modify.

OK!今天收工!回去洗澡!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chenglin_Yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值