网络编程,就是在两台或多台计算机之间通信,网络通信的三个要素:IP地址、端口号、协议。
socket所在层次示意图:
我们写的程序运行起来就是用户进程,我们的程序进行在运行时,如果要进行网络通信,只需要与socket进行交互就可以,socket封装了底层的协议与逻辑,使我们不必关心底层的实现,简化网络通信编程。
SOCKET编程:
涉及两方:服务器端和客户端。
服务器与客户端都需要创建socket。
python中创建socket,需要引入socket模块,然后使用socket方法创建:
import socket
sk = socket.socket(family,type,proto)
socket(family,type[,proto]) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。其中,family参数可选值如下:
名称 | 目的 |
---|---|
AF_INET | IPv4网络通信 |
AF_INET6 | IPv6网络通信 |
AF_PACKET | 链路层通信 |
AF_UNIX, AF_LOCAL | 本地通信 |
type参数
protocol参数:
0 (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议
IPPROTO_TCP | IPPTOTO_UDP | IPPROTO_SCTP | IPPROTO_TIPCTCP |
---|---|---|---|
TCP传输协议 | UDP传输协议 | STCP传输协议 | TIPC传输协议 |
也就是说,默认不输入参数时,socket()创建的是IPv4网络协议簇,流式socket,即TCP协议数据的一个进程。同socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM,proto=0) 语句等价。
创建好socket后,下一步要进行分叉了,就是要确定到低是作为服务器还是客户端,作为服务器,我们知道,服务器要对外提供服务,需要有确定的地址和端口,这个socket就要与作为服务器的地址和端口进行绑定,相当于变身为具体的服务器了,这个服务器的地址就是绑定的IP和端口号。客户端就可以通过这个地址进行链接。
服务端套接字函数:
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.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法:
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数:
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
服务器端:
运行后,在accept()处阻塞,即停止在此处,等待客户端连接的到来。运行如下客户端:
服务器端则接着执行,打印clisock:
(
server_tcpsock如下:
可以看到这是同一个sock,fd都是296,绑定地址与没绑定之前的区别就是多了laddr,即本地地址。
而接收客户端连接后,生成了新的sock,新的sock的fd是300,多了raddr,即客户端的地址,返回的clisock分成两部分,一部分是新的sock,一部分是客户端地址(IP+端口号),新的sock是以服务器的原sock为模板,通过增加raddr来生成的。这样服务器就有了客户端地址,就可以向客户端发送信息。
客户端是主动连接服务器,客户端是知道服务器的地址的,服务器是通过客户端的连接获得了客户端的地址。
通过程序,可以看到,只要客户端连接服务器,就在服务器端生成新的socket,不必发送信息。服务器与这个客户端的通信就使用这个socket,如果有其他客户端连接,会生成其他socket,相互的连接也是使用各自的socket,不会发生混乱。
clisock并不是socket,它是一个元组,包含了socket,所以,一般使用:
conn,addr = server_tcpsock.accept()
来分别获得socket和raddr。
看一下结果:
连接后,服务器和客户端都可以主动发送信息,但要做到一发一收。
客户端发送数据:
使用socket,应用TCP进行通信,数据必须是bytes类型的,所以需要进行转码:
服务器端接收:
看到接收后数据也是bytes型,显示时需要再次转换为字符串:
服务器端发送数据:
客户端接收:
客户端结果:
注意服务器的发送数据和接收数据,使用的socket都是conn,而不是server_tcpsock。
程序运行完毕后,连接是要关闭的,这里是Python自动为我们关闭的连接,一般我们需要在程序中主动关闭连接。对于服务器来说,关闭连接不是关闭所有的socket,而是关闭连接进来的socket,这里就是conn,因为服务器启动后是要不间断为所有客户端服务的,哪个客户端不使用了,就关闭哪个客户端的连接,也就是关系哪个客户端的socket。客户端只有一个socket,直接关闭这个socket就行了。
使用conn.close()关闭socket。
对于发送信息,有send()和sendall(),区别是,send()发送数据是有大小限制的,最大为发送缓冲区大小的数据,多出来的不发送,sendall()则反复循环使用send(),将所有数据全部发送。
recv()接收数据,参数用于指定接收缓冲区的大小,即一次最大接收多少数据。
recv()也有阻塞的作用,即一方启动后,可以不等到对方发送数据,先启动接收,这时因为对方还没发送数据,所以这时接收方就阻塞,一直在接收等待状态。
持续连接:
上述程序的BUG:
1、当客户端断开连接后,服务器端就应该跳出第二层while循环,否则服务器停在input处,输入信息发送,会使用已经断开的socket,发生错误。
2、客户端一开始就输入quit,直接终止时,服务器端也进行了接收,运行到input处,然后就跟第1条错误类似。
3、客户端连接以后,如果直接又异常终止了,服务器端会直接错误,接收语句使用了异常的socket。
那么,程序修改就针对上面的逻辑进行修改:当客户端主动结束时,服务器端接收的是空,可以此为判定条件,跳出内层循环;如果异常终止,在接收时就会出错,可以使用try语句捕获错误。
修改一:在这种交互中,不能发空,也就是不能直接回车,直接回车后,发送方因为是空,没有发送,又无法回到输入,卡死,接收端则一直处于接收阻塞,卡死
双方都增加一个判断输入为空的判断。
修改二:双方有一方正常结束时,另一方收到的是空,判断后结束:
修改三:客户端异常结束,服务器端要能判断并正常结束,同时服务器端在一个客户端终止后,循环连接其他客户端:
对于listen()方法,参数是连接池的大小,这里是2,就是可以有2个连接排队,即一个有3个连接:
关于多次运行同一程序的问题:在pycharm高版本中,需要如下操作:
右键点击程序,选择edit 'client'...,出现如下界面:
将Allow parallel run勾选上,就可以同时运行多个,即允许并行运行。