在学习socket编程之前,需要首先弄明白socket是什么,socket的作用,socket给网络带来了哪些改变。首先,我们知道在网络中通信的过程是两个终端之间的通信,而终端有一个唯一的标识,即IP地址,但是在一个终端中我们可能一边在浏览着网页,一遍在给别人发邮件等,而这些事情仅仅靠IP地址来区别是不行的,这时我们引入了传输层的端口来进行区别,即进程号。所以说网络的通信实则是不同进程之间的通信。
这时问题来了,网络中进程之间是如何通信的?
进程通信的概念最初来源于单机系统。即每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了多种方式:
a) 消息传递(管道(pipe)、FIFO、消息队列)
b) 同步(互斥量、信号量、读写锁)
c) 共享内存
目前在网络中通常使用TCP/IP协议的应用程序采用应用编程接口:UNIX BSD的套接字(socket),来实现网络中进程的通信。因而这也就体现出socket编程的重要性。那么什么是socket?由于socket起源于UNIX,而UNIX/LINUX设计的哲学就是“everything is file”,都可以用“open-write/read-close”的模式来操作。所以说socket其实也可以看作一个特殊的文件,通过一系列的函数来实现。
下面就开始简要地介绍socket的接口函数过程:
服务器端初始化socket,然后与端口绑定bind,对端口进行监听listen,调用accept阻塞,等待客户端连接。在这时客户端初始化一个socket,然后请求连接服务器connect,如果连接成功,这时客户端与服务器端的连接就建立起来了。客户端发送请求数据,服务器端接收请求并处理请求,接着把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
具体的socket接口函数可以去查询,这里就不给出具体的详解。
这里就解释下TCP/IP的socket三种类型套接字:
1. 流式套接字(SOCK_STEAM):提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。即TCP传输协议。
2. 数据包式套接字(SOCK_DGRAM):提供了一个无连接服务(UDP)。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。
3. 原始式套接字(SOCK_RAW):该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
接下来简要介绍socket中TCP的三次握手和四次释放过程:
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
接下来附上TCP client和TCP server端的Python代码:<span style="font-size:14px;">#!/usr/bin/env python
# -*- coding: utf-8 -*- #将编码设置为utf-8
'a socket example which send echo message to server.'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #其中AF_INET表示IPv4地址,AF_INET6表示IPv6地址
# 建立连接:
s.connect(('127.0.0.1', 9999)) #客户端请求连接127.0.0.1的9999端口,最好不要选择1024之前的
# 接收欢迎消息:
print s.recv(1024)
for data in ['Bob','John', 'Sara','Tom']:
# 发送数据:
s.send(data) #多个请求,服务器端需要建立多线程
print s.recv(1024)
s.send('exit') #数据发送完毕
s.close()
</span>
<span style="font-size:14px;">#!/usr/bin/env python
# -*- coding: utf-8 -*-
'a server example which send hello to client.'
import time, socket, threading #库文件
def tcplink(sock, addr):
print 'Accept new connection from %s:%s...' % addr
sock.send('Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if data == 'exit' or not data:
break
sock.send('Hello, %s!' % data)
sock.close()
print 'Connection from %s:%s closed.' % addr
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #建立 一个socket
# 监听端口:
s.bind(('127.0.0.1', 9999))
s.listen(5) #设置监听的包为5
print 'Waiting for connection...'
while True:
# 接受一个新连接:
sock, addr = s.accept() #包括IP地址和sock端口号
# 创建新线程来处理TCP连接:
t = threading.Thread(target=tcplink, args=(sock, addr)) #线程处理的方式为tcplink
t.start()
</span>
接下来附上UDP client和UDP server端的Python代码:
<span style="font-size:14px;">#!/usr/bin/env python
# -*- coding: utf-8 -*-
'a socket example which send echo message to server.'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #UDP无需建立连接,直接发送数据
for data in ['Bob', 'John', 'Sara','Tom']:
# 发送数据:
s.sendto(data, ('127.0.0.1', 9999)) #将数据发送到服务器127.0.0.1的9999端口的进程
# 接收数据:
print s.recv(1024)
s.close()
</span>
<span style="font-size:14px;">#!/usr/bin/env python
# -*- coding: utf-8 -*-
'a udp server example which send time to client.'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999)) #服务器仅仅需要绑定一个地址和端口号
print 'Bind UDP on 9999...'
while True:
# 接收数据:
data, addr = s.recvfrom(1024)
print 'Received from %s:%s.' % addr
s.sendto('Hello, %s!' % data, addr)
</span>
以上整个socket编程的初步工作已经完成,通过socket编程更加来了解TCP、UDP两种协议,这也告诉我们网络中的协议的实现都是需要编程来实现的。