套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
socket能干嘛?
为什么学习socket需要懂得互联网协议?
socket发展史及分类
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。
因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同
一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX,unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信。
基于网络类型的套接字家族
套接字家族的名字:AF_INET,还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET。
既然是C/S架构,那么socket编程就需要实现两部分,一个是服务端,另一个是客户端。先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束(注意,就是指的是基于TCP的套接字,UDP协议的套接字更简单一些。)可以参考下图:
具体到python中代码实现socket编程,主要是通过 socket 这个模块,这里做一个简单介绍:
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是
SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.close() 关闭套接字
服务端:
# author:dayin
import socket
# 第一个参数 表示 基于网络类型的套接字家族 第二个参数表示 基于流式的 可以简单理解为 基于TCP协议
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size=1024
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind(ip_port)
server.listen(back_log)
print('>>>>等待客户端连接...')
# 阻塞 等待 客户端连接
conn, address = server.accept()
print(address)
# 收到 1024个字节信息
message = conn.recv(buffer_size)
print('>>>>接收到客户端发来的数据:{}'.format(message.decode()))
# 发送数据给客户端
conn.send(message.upper())
conn.close()
server.close()
客户端:
# author:dayin
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size=1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)
message = input('>>>请输入发送给服务端消息:')
client.send(message.encode())
rev_msg = client.recv(buffer_size)
print('收到服务器消息:{}'.format(rev_msg.decode()))
client.close()
代码实现如下图:
启动TCP服务端:
可以看出,服务端程序,阻塞在 server.accept(),等待 着客户端连接。
启动TCP客户端:
可以看出,此时服务端程序,又阻塞在 conn.recv(buffer_size) 这里,等待着客户端向服务端发送数据。
当客户端发送数据给服务端后,服务端和客户端都终止运行了。此时,服务端收到客户端的数据"hello world",并将其转化为大写字符"HELLO WORLD"并传送给客户端,这样,就是实现了 服务端和客户端的一次通信。
But,这很显然不符合C/S架构,C/S架构需要实现的是,服务端一直运行下去,不能因为客户端的断开,而服务端也要断开连接。此外,服务端和客户端只进行一次通信,这也是不合理的。这时,就需要加入 链接循环(为了使得服务器一直运行下去,等待着客户端的连接。) 与 消息循环(为了实现客户端和服务端的多次通信)。
服务端:
# author:dayin
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
backlog = 5
# 第一个参数 表示 基于网络类型的套接字家族 第二个参数表示 基于流式的 可以简单理解为 基于TCP协议
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind(ip_port)
# listen(5) 表示 最多可以有5个客户接入,这里的接入应指的是等待
server.listen(backlog)
# 链接循环
while True:
print('>>>>等待客户端连接...')
# 阻塞 等待 客户端连接
conn, address = server.accept()
print('客户端>>>', address, '连接>>>>')
# 消息循环
while True:
message = conn.recv(buffer_size)
if not message: break
print('>>>>接收到客户端发来的数据:{}'.format(message.decode()))
# 发送数据给 客户端
conn.send(message.upper())
conn.close()
客户端:
# author:dayin
# Date:2019/7/26 0026
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)
while True:
message = input('>>>请输入发送给服务端消息(输入quit退出链接):')
if message == 'quit': break
client.send(message.encode())
rev_msg = client.recv(buffer_size)
print('收到服务器消息:{}'.format(rev_msg.decode()))
client.close()
结果如下图:
客户端正常退出:
服务端此时并未退出,而是继续监听,等待下一个客户端连接:
PS:
if not messge: continue
基于UDP套接字的简单示例(聊天室)
由于UDP是无连接的,所以可以同时多个客户端去跟服务端通信。也就不存在链接循环。
服务端:
# author:dayin
import socket
ip_port = ('192.168.1.2', 8080)
buffer_size = 1024
# UDP 是数据报格式
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报
# 绑定ip 和 端口
server.bind(ip_port)
print('等待客户端连接....')
# 服务端循环,等待客户端连接
while True:
msg, addr = server.recvfrom(buffer_size)
print('收到客户端 ip_port为{},收到消息为 {}'.format(addr, msg.decode()))
# 发送消息给客户端
server.sendto(msg.upper(), addr)
客户端:
# author:dayin
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ('192.168.1.2', 8080)
buffer_size = 1024
while True:
msg = input('>>>>').strip()
# 发送消息给服务端
client.sendto(msg.encode(), ip_port)
# 接收服务端的消息
data, addr = client.recvfrom(buffer_size)
print('收到服务端的消息为:', data.decode())
结果如下图:
启动服务端:
PS: