在上一篇博客中,我们先对套接字编程的内容进行了一个简单涵盖,并详细陈述了UDP协议内容。本篇我们承接上文,讲述完UDP后,我们来讲解TCP。
目录
1.TCP协议
1.1通信两端流程
1.1.1服务端流程
1.1.2客户端流程
1.2套接字相关操作接口
1.2.1创建套接字
1.2.2绑定地址信息
1.2.3设置监听
1.2.4发送连接建立请求
1.2.5获取新建连接
1.2.6发送数据
1.2.7接收数据
1.2.8关闭套接字
1.3代码实现
1.TCP协议
TCP是传输控制协议,一种面向连接,可靠传输,提供字节流传输的服务。
1.1通信两端流程
1.1.1服务端流程
首先依旧是创建套接字,即在内核中创建一个socket结构体;然后为创建的套接字绑定地址信息;其次开始监听,对于TCP而言,它是一个存在状态的结构体,不同的状态下只能进行特定的操作,开始监听就是将TCP套接字的状态制为listen状态,只有在listen状态下时,服务端才会处理客户端的连接请求。
具体情况便是:客户端向服务端发送一个连接请求后,当服务端不处于监听listen状态时,则会丢弃该数据,不做任何处理;
当服务端处于监听状态时,则会立即为这个连接建立请求处理:为该新连接建立请求,创建一个套接字结构体,并为该新创建的套接字,描述完整的五元组信息。此后的数据通信都有这个新创建的套接字进行通信,这也就是说,一开始创建的套接字只负责新连接处理请求,不负责数据通信,并且当后续有多少客户端和服务端建立连接,便会创建多少套接字。
在监听后,是获取新连接,即从监听套接字对应的新建队列中,取出一个套接字结构体,并返回该套接字的描述符;连接建立之后,便可以开始收发数据,收发数据过程中我们所使用的套接字描述符都是从获取新连接中的得到的;最后便是关闭套接字,释放资源。
值得注意的是,TCP的数据通信当中,我们不限定必须由客户端先发送数据。因为在连接建立中,新套接字socket中已经存在了完整的五元组,也正因如此,TCP通信中,我们不需要指定对端地址。
1.1.2客户端流程
首先是创建套接字;然后是为套接字绑定地址信息(不推荐);其实是向服务端发送连接建立请求,当连接一旦建立成功,socket中就会描述一个完整的五元组信息;再是收发数据;最后关闭套接字,释放资源。
1.2套接字相关操作接口
1.2.1创建套接字
int socket(int domain, in type, int protocol);
socket是套接字创建接口,和UDP协议不同的是,我们传入TCP协议所需的参数即可实现TCP通信。
其中domain是地址域类型,ipv4 -- AF_INET;type是套接字类型,TCP选择字节流传输--流式套接字--SOCK_STREAM;protocol是协议类型--选择TCP协议--IPPROTO_TCP。
1.2.2绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
1.2.3设置监听
int listen(int sockfd, int backlog);
listen是设置服务端为监听状态的接口,其中sockfd是创建套接字返回的监听套接字描述符;backlog是同一时刻最大并发连接数。(限制同一时刻最多有多少客户端的连接建立请求能被处理)
返回值:监听成功则返回0;失败则返回-1。
设置最大并发连接数的目的在于,避免TCP服务器对过多客户端的新连接创建socket,socket创建过多会导致服务器资源耗尽,系统崩溃。
具体做法便是:内核中每一个监听套接字都有一个新连接的socket队列,会将队列分为两类,半连接队列和已连接队列,backlog参数便是对队列的节点数量进行限制。(队列节点数量 = backlog + 1)(一旦半连接队列被放满,再又新连接到来则服务器会直接丢弃)
1.2.4发送连接建立请求
int connet(int sockfd, struct sockaddr* addr, socklen_t len);
connet是客户端向服务端发送连接建立请求接口(只有客户端会用到),其中sockfd是套接字描述符,addr是服务端地址信息,addrlen是地址长度。
1.2.5获取新建连接
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
accept是从内核sockfd指定的监听套接字中对应的已完成连接队列中取出一个socket接口,其中addr是accpet内部进行填充的客户端地址,是一个输出参数;addrlen是地址信息长度,是一个输入输出参数,用于指定想要获取的地址长度,以及返回实际的地址长度。
返回值:获取成功则返回新建连接的套接字描述符;失败返回-1。
1.2.6发送数据
ssize_t send(int sockfd, void* data, size_t len, int flag);
send是发送数据接口,和sendto唯一不同在于不需要指定对端地址。
返回值:发送成功则返回实际发送的数据字节长度;出错返回-1。
1.2.7接收数据
ssize_t recv(int sockfd, void* buf, size_t len, int flag);
recv是接收数据接口,和recvfrom唯一不同在于不需要获取对端地址。
返回值:接收成功则返回实际发送的数据字节长度;出错返回-1;连接断开则返回0。
1.2.8关闭套接字
int close(int sockfd);
1.3代码实现
我们封装TcpSocket类,来对socket操作进行封装,简化后续使用难度,提高代码泛用性。
然后我们对封装的类进行使用,先设计出服务端代码如下:
再设计出客户端代码如下:
最后我们进行测试:
很明显当客户端多次请求发送数据时,客户端会直接退出,并发出“connect broken”的打印字样。这是因为我们在服务端设计代码中,获取新连接之后所创建连接中的“new_sock"是一个局部变量,当循环结束时它的生命周期也随之结束。
如此便导致服务器与一个客户端通信一次之后,下次再进行通信获取到创建的客户端是新的局部变量:“new_sock"。
对于上述问题的解决,我们才可以采用多进程或多线程的方式来解决,即处理TCP服务器中多个客户端连接时创建多个socket的情况。将执行流拆分开,对于创建新的socket我们进一步创建子进程或者线程来完成对其的处理,进而实现TCP协议中的多客户端与同一个服务端的稳定通信。
具体解决思路和代码设计我们在下一篇博客中进行说明~