socket通信的原理与实践
主要参考了以下几篇博客,学到了很多,在这里总结一下
TCP网络编程中connect()、listen()和accept()三者之间的关系
TCP/IP协议是我们熟知的传输层协议,socket与TCP/IP协议模型的关系如下:
Socket是什么呢?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
简介Socket通信的全过程:
1.从服务器端说起,服务器端先初始化一个socket,调用bind()函数绑定一个端口
2.然后调用listen()函数监听端口,(相当于服务器的客服是等待着客户(相当于客户端)电话的到来),listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。
这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。
3.此时,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。
4.accept()函数从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
5.此时客户端与服务端的连接就已经建立了,可以进行网络I/O操作了,即类同于普通文件的读写I/O操作。
图示:
详细介绍TCP的Socket有关函数
socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket()函数:
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
bind()函数:
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
listen()函数:
listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长,TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。
所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。
此处重点介绍一下listen()函数的参数:
int listen(int sockfd, int backlog)
第二个参数告诉内核连接队列的长度,为了更好的理解 backlog 参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:
1、未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。
2、已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。
当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。
如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。
accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址数据,第三个参数为协议地址的长度。
注意accept返回的是一个内核生成的全新的描述字,是已连接的描述字。accept参数列表的socket描述字是服务器调用socket()生成的称为监听socket描述字,在服务器生命周期内始终存在。
在服务器端,socket()返回的套接字用于监听(listen)和接受(accept)客户端的连接请求。这个套接字不能用于与客户端之间发送和接收数据。accept()接受一个客户端的连接请求,并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。
再次调用accept()可以接受下一个客户端的连接请求,并再次返回一个新的套接字(与socket()返回的套接字、之前accept()返回的套接字都不同的新的套接字)。这个新的套接字用于与这次接受的客户端之间的通信。
假设一共有3个客户端连接到服务器端。那么在服务器端就一共有4个套接字:第1个是socket()返回的、用于监听的套接字;其余3个是分别调用3次accept()返回的不同的套接字。如果已经有客户端连接到服务器端,不再需要监听和接受更多的客户端连接的时候,可以关闭由socket()返回的套接字,而不会影响与客户端之间的通信。当某个客户端断开连接、或者是与某个客户端的通信完成之后,服务器端需要关闭用于与该客户端通信的套接字。
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网络中不同进程之间的通信!
用python语言实现简单的socket通信
代码如下:
#server端代码
import socket #创建套接字 sk=socket.socket() #创建ip地址与端口号组成的元组 addr=("127.0.0.1",2333) #调用bind函数为socket绑定一个端口 sk.bind(addr) #设定侦听队列的长度 sk.listen(1024) while True: print("prepared for connection") #调用accept函数监听端口,阻塞到有连接 #accept返回客户端地址信息和已连接socke描述符 coon,address=sk.accept() if coon: print("connect confirm") else: break while True: #接收数据,每次从缓冲区取1024个字节 data=coon.recv(1024) receive=data.decode() print("client: "+receive) #发送数据 s_data=input("input something to send: ") coon.send(s_data.encode()) coon.send(s_data.encode())
#客户端代码 import socket #创建客户端socket,不需要bind端口 sk=socket.socket() addr=("127.0.0.1",2333) try: #建立连接 sk.connect(addr) print("connect confirm") while True: #发送数据 s_data=input("input something to send: ") sk.send(s_data.encode()) #接收数据 data=sk.recv(1024) receive=data.decode() print("server: "+receive) except Exception as e: print("connenct unsuccess") sk.close()
运行结果
服务端:
客户端: