Socket网络编程入门
引子
从我们熟悉的web应用为例子,先不讨论socket,我们看一下如果两台机器之间要通讯,需要经过哪些步骤。我们看一下服务器回给我们一个简单的”hi”时,需要经过哪些步骤。
服务端:
1.服务器的应用层会产生一个hi字符串
2.数据封装后交给传输层
3.传输层封装后交给网络层
4.网络层数据封装后交给数据链路层
5.物理层接收后变成0和1字节流传输到internet网络中
客户端:
1.客户端物理层接收来自服务端的0和1字节流
2.数据链路层接收物理层数据并解读数据报
3.网络层接收物理层数据并解读数据报
4.传输层解读网络层数据并解读数据报
5.应用层接收到hi字符
现实中的网络数据传输远远比这复杂得多,数据必须经过5层协议栈。试想如果让一个开发者从0开始实现这样的一个数据传输,难度可想而知。但是目前从事网络开发的人千千万万,他们是怎么做到的呢,这一切都要归功于unix操作系统和socket。因为有了socket,网络编程变得快速而且简单了很多。
什么是socket
socket就是操作系统(unix)为应用进程提供的一个方便操作网络数据收发的api接口,只要你读懂了这一套api接口,就可以把数据从一个主机传输到另一个主机,或者从其他主机接受数据,而不需要知道还有五层协议的复杂性。这就好比我们开发者读取文件的时候,file系列函数就可以轻松的帮我们获取文件的内容,我们不需要知道磁盘的磁头是如何转动,如何实现定位读数据等。
这个图的上层是应用程序层,比如我们的nginx,apache系列的软件,下层是操作系统层,比如Unix。Socket层位于应用层和传输层之间,上层应用如果想实现网络编程,只需要调用操作系统提供的Socket层提供的api即可。操作系统通过Socket大大简化了网络编程的复杂性,使得从事网络编程的开发人员更加便捷。
Socket相关函数一览
既然知道了Socket是操作系统为我们提供的一层网络编程接口封装,那就可以看一下操作系统给我们留了哪些接口,我们能用这些接口干什么
函数名称 | 用途 | 说明 |
---|---|---|
socket | 创建套接字 | 服务端/客户端需要使用 |
connect | 连接远端服务器 | 仅客户端 |
bind | 绑定套接字的本地ip和端口号 | 通常客户端不需要 |
listen | 置服务端套接字为监听模式 | 仅服务端可用 |
accept | 接收一个连接请求并创建新套接字 | 仅服务端可用 |
send | 发送数据 | 服务端和客户端可用 |
recv | 接收数据 | 服务端和客户端可用 |
close | 关闭套接字 | 服务端和客户端可用 |
客户端编程
创建 socket
首先要创建 socket,用 Python 中 socket 模块的函数 socket
就可以完成:
1 | #Socket client example in python |
函数 socket.socket
创建一个 socket,返回该 socket 的描述符,将在后面相关函数中使用。该函数带有两个参数:
- Address Family:可以选择
AF_INET
(用于 Internet 进程间通信) 或者AF_UNIX
(用于同一台机器进程间通信) - Type:套接字类型,可以是
SOCKET_STREAM
(流式套接字,主要用于 TCP 协议)或者SOCKET_DGRAM
(数据报套接字,主要用于 UDP 协议)
错误处理
如果创建 socket 函数失败,会抛出一个 socket.error
的异常,需要捕获:
1 | #handling errors in python socket programs |
连接服务器
本文开始也提到了,socket 使用 (IP地址,协议,端口号) 来标识一个进程,那么我们要想和服务器进行通信,就需要知道它的 IP地址以及端口号。
获得远程主机的 IP 地址
Python 提供了一个简单的函数 socket.gethostbyname
来获得远程主机的 IP 地址:
1 | host = 'www.google.com' |
现在我们知道了服务器的 IP 地址,就可以使用连接函数 connect
连接到该 IP 的某个特定的端口上了,下面例子连接到 80 端口上(是 HTTP 服务的默认端口):
1 | #Connect to remote server |
运行该程序:
1 | $ python client.py |
发送数据
上面说明连接到 www.google.com 已经成功了,接下面我们可以向服务器发送一些数据,例如发送字符串GET / HTTP/1.1\r\n\r\n
,这是一个 HTTP 请求网页内容的命令。
1 | #Send some data to remote server |
发送完数据之后,客户端还需要接受服务器的响应。
接收数据
函数 recv
可以用来接收 socket 的数据:
1 | #Now receive data |
一起运行的结果如下:
1 | Socket created |
关闭 socket
当我们不想再次请求服务器数据时,可以将该 socket 关闭,结束这次通信:
1 | s.close() |
小结
上面我们学到了如何:
- 创建 socket
- 连接到远程服务器
- 发送数据
- 接收数据
- 关闭 socket
当我们打开 www.google.com 时,浏览器所做的就是这些,知道这些是非常有意义的。在 socket 中具有这种行为特征的被称为CLIENT,客户端主要是连接远程系统获取数据。
socket 中另一种行为称为SERVER,服务器使用 socket 来接收连接以及提供数据,和客户端正好相反。所以 www.google.com 是服务器,你的浏览器是客户端,或者更准确地说,www.google.com 是 HTTP 服务器,你的浏览器是 HTTP 客户端。
那么上面介绍了客户端的编程,现在轮到服务器端如果使用 socket 了。
服务器端编程
服务器端主要做以下工作:
- 打开 socket
- 绑定到特定的地址以及端口上
- 监听连接
- 建立连接
- 接收/发送数据
上面已经介绍了如何创建 socket 了,下面一步是绑定。
绑定 socket
函数 bind
可以用来将 socket 绑定到特定的地址和端口上,它需要一个 sockaddr_in
结构作为参数:
1 | import socket |
绑定完成之后,接下来就是监听连接了。
监听连接
函数 listen
可以将 socket 置于监听模式:
1 | s.listen(10) |
该函数带有一个参数称为 backlog,用来控制连接的个数。如果设为 10,那么有 10 个连接正在等待处理,此时第 11 个请求过来时将会被拒绝。
接收连接
当有客户端向服务器发送连接请求时,服务器会接收连接:
1 | #wait to accept a connection - blocking call |
运行该程序的,输出结果如下:
1 | $ python server.py |
此时,该程序在 8888 端口上等待请求的到来。不要关掉这个程序,让它一直运行,现在客户端可以通过该端口连接到 socket。我们用 telnet 客户端来测试,打开一个终端,输入 telnet localhost 8888
:
1 | $ telnet localhost 8888 |
这时服务端输出会显示:
1 | $ python server.py |
我们观察到客户端已经连接上服务器了。在建立连接之后,我们可以用来与客户端进行通信。下面例子演示的是,服务器建立连接之后,接收客户端发送来的数据,并立即将数据发送回去,下面是完整的服务端程序:
1 | import socket |
在一个终端中运行这个程序,打开另一个终端,使用 telnet 连接服务器,随便输入字符串,你会看到:
1 | $ telnet localhost 8888 |
客户端(telnet)接收了服务器的响应。
我们在完成一次响应之后服务器立即断开了连接,而像 www.google.com 这样的服务器总是一直等待接收连接的。我们需要将上面的服务器程序改造成一直运行,最简单的办法是将 accept
放到一个循环中,那么就可以一直接收连接了。
保持服务
我们可以将代码改成这样让服务器一直工作:
1 | import socket |
现在在一个终端下运行上面的服务器程序,再开启三个终端,分别用 telnet 去连接,如果一个终端连接之后不输入数据其他终端是没办法进行连接的,而且每个终端只能服务一次就断开连接。这从代码上也是可以看出来的。
这显然也不是我们想要的,我们希望多个客户端可以随时建立连接,而且每个客户端可以跟服务器进行多次通信,这该怎么修改呢?
处理连接
为了处理每个连接,我们需要将处理的程序与主程序的接收连接分开。一种方法可以使用线程来实现,主服务程序接收连接,创建一个线程来处理该连接的通信,然后服务器回到接收其他连接的逻辑上来。
1 | import socket |
再次运行上面的程序,打开三个终端来与主服务器建立 telnet 连接,这时候三个客户端可以随时接入,而且每个客户端可以与主服务器进行多次通信。
telnet 终端下可能输出如下:
1 | $ telnet localhost 8888 |
要结束 telnet 的连接,按下 Ctrl-]
键,再输入 close
命令。
服务器终端的输出可能是这样的:
1 | $ python server.py |