《计算机网络:自顶向下方法》阅读笔记系列
学习套接字编程就是探讨一下网络应用程序是如何实际编写的。
先回顾一下套接字:网络应用是由一对程序(客户程序和服务器程序)组成,位于两个不同的端系统中。两个程序运行时,创建了一个客户进程、一个服务器进程,他们就是通过套接字读出或者是写入数据从而实现在彼此之间进行通信。
UDP套接字编程
在发送进程能够将数据分组推出套接字之前,当使用UDP时,必须先将目的地址附在该分组之上,目的地址包括:
- 目的主机的IP地址(使得路由器能够通过因特网将分组路由到目的主机)
- 该套接字的端口号(port number)(因为一台主机 可能运行许多网络应用进程,每个进程具有一个或多个套接字,因此当生成一个套接字时,就为它分配一个成为**端口号(port number)**的标识符)
除此之外,发送方的源地址也要附在该分组上,与目的地址类似,源地址包括源主机的IP地址和源套接字的端口号。但是,将该源地址附在分组之上的操作通常不是UDP应用程序代码所为,而是由底层操作系统自动完成。
演示示例:
1)客户从其键盘读取一行字符(数据)并将该数据向服务器发送
2)服务器接受该数据并将这些字符转换为大写
3)服务器将修改的数据发送给客户
4)客户接受修改的数据并在其监视器上将该行显示出来
简单的程序实现:
1.UDPClient.py
from socket import *
serverName = 'hostname'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_DGRAM)
message = raw_input('Input lowercase sentense:')
clientSocket.sendto(message.encode(),(serverName, serverPort))
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
print(modifiedMessage.decode())
clientSocket.close()
from socket import *
socket模块形成了在Python中所有网络通信的基础,使得我们能够在程序中创建套接字。
serverName = 'hostname'
serverPort = 12000
将serverName置为服务器的IP地址或者服务器的主机名,第二行将serverPort置为服务器套接字的端口号。
clientSocket = socket(AF_INET, SOCK_DGRAM)
创建客户的套接字clientSocket,第一个参数指示了地址簇,AF_INET指示了底层网络使用了IPv4,第二个参数指示了该套接字是SOCK_DGRAM类型的,意味着它是一个UDP套接字。
创建套接字时,没有指定客户套接字的端口号,相反操作系统做了这件事。
message = raw_input('Input lowercase sentense:')
采用Python的内置功能,将用户在键盘上的输入存入到message变量中。
在python3.x中对raw_input()和input()进行了整合,去除了raw_input(),仅保留了input()函数。
clientSocket.sendto(message.encode(),(serverName, serverPort))
有了套接字和报文之后,就通过该套接字向目的主机发送报文,message.encode()
的作用是将报文从字符串类型转换为字节类型,因为需要向套接字中发送字节。sendto
方法为报文附上目的地址并且向进程的套接字clientSocket发送结果分组。
在发送完分组之后,客户就等着接受来自服务器的数据。
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
当客户套接字收到一个来自因特网的分组时,该分组的数据被放置到变量modifiedMessage中,其源地址被放置到变量serverAddress中。
print(modifiedMessage.decode())
这行代码将报文从字节转化为字符串后,在用户的显示器上打印出来。
clientSocket.close()
最后关闭了套接字,然后关闭了该进程。
2.UDPServer.py
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('', serverPort))
print("The server is ready to receive")
while True:
message, clientAddress = serverSocket.recvfrom(2048)
modifiedMessage = message.decode().upper()
serverSocket.sendto(modifiedMessage.encode(), clientAddress)
代码开始部分与客户端类似,有很大不同的第一行代码是:
serverSocket.bind(('', serverPort))
将端口号12000与该服务器的套接字绑定在一起,说明了在服务器端为套接字分配一个端口号是代码显式实现的。这样,任何向该服务器的IP地址的端口12000发送的分组都将导向该套接字。
message, clientAddress = serverSocket.recvfrom(2048)
在接收到一个分组时,数据被放置到变量message中,其源地址被放置到clientAddress中。
modifiedMessage = message.decode().upper()
这一行代码属于业务代码,即将报文转化为字符串后,使用upper()方法将其转换为大写。
serverSocket.sendto(modifiedMessage.encode(), clientAddress)
最后将回答报文编码为字节类型后返回给客户端。
TCP套接字编程
TCP是一个面向连接的协议,意味着客户和服务器在能够开始互相发送数据之前,先要握手创建一个TCP连接。TCP连接一端与客户套接字相联系,另一端与服务器套接字相联系。在创建TCP连接时,需要将其与客户套接字地址(IP地址和端口号)和服务器套接字地址(IP地址和端口号)关联起来。使用创建的TCP连接,当一侧要向另一侧发送数据时,它只需经过其套接字将数据丢进TCP连接,而UDP在将分组丢进套接字之前必须为其附上一个目的地地址。
TCP中客户程序与服务器程序的交互过程
客户具有向服务器发起接触的任务。服务器为了能够对客户的初始接触作出反应,服务器必须已经准备好。这意味着:
- 与UDP中的情况一样,TCP服务器在客户试图发起接触前必须作为进程运行起来
- 服务器程序必须具有一个特殊的套接字,该套接字欢迎来自运行在任意主机上的客户进程的某种初始接触
在服务器进程的运行过程中,客户进程能够向服务器发起一个TCP连接,这是由客户程序通过创建一个TCP套接字完成的。在客户生成其TCP套接字时,指定了服务器中的欢迎套接字的地址,即服务器主机的IP地址及其套接字的端口号。生成其套接字后,该客户发起一个三次握手并创建与服务器的一个TCP连接。
三次握手是发生在客户进程套接字与服务器进程的欢迎套接字之间的,在这期间,服务器将生成一个新的套接字,也就是图中的连接套接字,它专门用于特定的客户。
注意区分:
- 欢迎套接字:这是所有要与服务器通信的客户的起始接触点
- 连接套接字:这是随后为与每个客户通信而生成的套接字
从应用程序的观点来看,客户套接字和服务器连接套接字直接通过一根管道连接。客户进程不仅能向它的套接字发送字节,也能从中接收字节;服务器进程不仅能从它的连接套接字接收字节,也能向其发送字节。TCP保证了其发送和接收字节的可靠性。
简单的程序实现
1.TCPClient.py
from socket import *
serverName = 'servername'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName, serverPort))
sentence = raw_input('Input lowercase sentence:')
clientSocket.send(sentence.encode())
modifiedSentence = clientSocket.recv(1024)
print('From Server:', modifiedSentence.decode())
clientSocket.close()
clientSocket = socket(AF_INET, SOCK_STREAM)
该行创建了客户的套接字,第一个参数仍指示底层网络使用的是IPv4,第二个参数指示该套接字是SOCK_STREAM类型,表明它是一个TCP套接字。
同样,创建该客户套接字时仍未指定其端口号,因为这个操作仍是由操作系统替我们完成的。
clientSocket.connect((serverName, serverPort))
上面这一行就发起了客户和服务器之间的TCP连接,这行代码执行完后,执行三次握手,并在客户和服务器之间创建起一条TCP连接。
sentence = raw_input('Input lowercase sentence:')
与UDPClient一样,这一行是从用户获得一个句子。
clientSocket.send(sentence.encode())
通过该客户的套接字并进入TCP连接发送字符串sentence,这里没有显式地创建一个分组并为该分组附上目的地址,这是与UDP不同的一点。
modifiedSentence = clientSocket.recv(1024)
接收服务器传回来的字节。
clientSocket.close()
最后一行关闭套接字。
2.TCPServer.py
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', serverPort))
serverSocket.listen(1)
print('The server is ready to receive')
while True:
connectionSocket, addr = serverSocket.accept()
sentence = connectionSocket.recv(1024).decode()
capitalizedSentence = sentence.upper()
connectionSocket.send(capitalizedSentence.encode())
connectionSocket.close()
serverSocket = socket(AF_INET, SOCK_STREAM)
与TCPClient相同的是,服务器创建了一个TCP套接字。
serverSocket.bind(('', serverPort))
与UDPServer类似,将服务器的端口号serverPort与该套接字关联起来。
serverSocket.listen(1)
serverSocket是我们的欢迎套接字,创建好这个欢迎套接字后,将等待并聆听某个客户发出的连接请求,其中的参数定义了请求连接的最大数(至少为1)。
connectionSocket, addr = serverSocket.accept()
当客户发出请求后,程序为serverSocket调用accept()方法,这在服务器中创建了一个称为connectionSocket的新套接字,由这个特定的客户专用。
connectionSocket.close()
在此程序中,执行完业务代码之后,就关闭了该连接套接字,但由于serverSocket保持打开,所以其他客户仍然可以向该服务器发出连接请求。