文章目录
1. TCP协议通信流程
1.1 初始化
服务器:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来。
客户端:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求。
1.2 建立连接 —— 三次握手
首先客户端和服务端都是关闭状态,也就是CLOSED状态。服务器端进入一个监听的LISTEN状态,阻塞等待客户端的连接。
- 第一次握手:客户端向服务端发送一个SYN的标志位以请求连接。此时客户端进入SYN_SEND状态,也就是开始阻塞等待服务器的应答。
- 第二次握手:服务端收到了客户端的SYN连接请求,也就处于SYN_RCVD状态。由于现在客户端向服务端单方面请求连接了,要想双方都建立连接的话,就需要服务端也向客户端发送一个SYN请求。同时,服务端还需要响应给客户端一个ACK应答,告诉客户端接收到了你的连接请求并同意建立连接。
- 第三次握手:客户端接收到服务端的ACK应答和SYN连接之后,说明连接已经成功的建立,所以客户端处于ESTABLISHED状态。但是此时还需要返回给服务端一个ACK应答,告诉服务器我收到了你的连接请求。当服务端接收到最后一个ACK应答之后,就说明双方已经建成了连接通信,服务端也处于一个EXTABLISHED状态。
流程如图所示:
这个建立连接的过程, 通常称为三次握手。
1.3 数据传输
-
建立连接后, TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。
-
服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。
-
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答。
-
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。
-
客户端收到后从read()返回, 发送下一条请求, 如此循环下去。
1.4 关闭连接 —— 四次挥手
在数据传输之后,由主动断开连接方来进行请求关闭连接。
- 第一次挥手:主动断开连接方调用close()方法关闭连接,向被动断开连接方发送一个FIN关闭连接请求,此时主动断开连接方就处于FIN_WAIT1状态。
- 第二次挥手:被动断开连接方收到主动断开连接方的FIN之后,处于CLOSE_WAIT状态,说明被动断开连接方要准备关闭连接了。同时会先返回一个ACK应答,告诉主动断开连接方,我收到了你的关闭连接请求。此时收到ACK应答的主动断开连接方处于一个FIN_WAIT2状态,也就是继续等待的状态。
- 第三次挥手:被动断开连接方调用close()方法关闭连接,发送给主动断开连接方一个FIN结束报文段。此时被动断开连接方会进入LAST_ACK的状态,等待最后一个ACK的到来,以确定客户端收到了服务器发送的FIN。
- 第四次挥手:主动断开连接方收到被动断开连接方发来的FIN结束报文段之后,进入TIME_WAIT状态,同时发送给被动断开连接方最后一个ACK响应,告诉被动断开连接方我收到了你的结束报文段,并等待2倍的报文最大生存时间之后,确保被动断开连接方收到最后一个ACK,再进入CLOSED状态,完成连接关闭。同时被动断开连接方收到最后一个ACK应答之后,状态转换为CLOSED状态,彻底关闭连接。
流程如图所示:
这个断开连接的过程, 通常称为四次挥手。
2. 简单的TCP网络程序
2.1 TCP的socket API详解
2.1.1 监听
当调用监听之后,意味着告诉操作系统,当前进程可以接收新的TCP连接了。
换句话说,告诉操作系统,当前进程可以开始接收客户端的三次握手请求了。
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
参数:
sockfd: 套接字描述符
backlog: 已完成连接队列的大小,引申含义:并发连接数
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5);
- listen()成功返回0, 失败返回-1。
2.1.2 获取连接
如果服务端在获取连接的时候,目前没有新的连接,则该接口会阻塞等待。
// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd: 套接字描述符,传递的是侦听套接字
addr: 对端地址信息
addrlen: 对端地址信息长度
返回值: 返回为新连接创建的套接字描述符
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求, 就阻塞等待直到有客户端连接上来;
- addr是一个传出参数, accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL, 表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度。
2.1.3 建立连接
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd :套接字描述符
addr :服务端的地址信息
addrlen : 地址信息长度
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0, 出错返回-1。
2.1.4 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd: 套接字描述符
客户端: socket函数的返回值
服务端: 服务端为客户端新创建的套接字描述符
buf: 待发送的数据
len: 发送数据的长度
flags: 0 表示阻塞发送
2.1.5 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags) ;
参数:
sockfd: 套接字描述符
客户端: socket函数的返回值
服务端: 服务端为客户端新创建的套接字描述符
buf: 缓冲区,用来接收数据
len: 接收的最大能力
flags : 0 表示阻塞接收,MSG_PEEK 表示探测接收
返回值:
大于0: 表示接收了多少字节的数据
等于0: 表示对端断开了连接,意味着对端调用了close
小于0: 接收失败
2.2 客户端程序
创建套接字,发起连接,发送数据,接收数据,关闭套接字。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(19999);
dest_addr.sin_addr.s_addr = inet_addr("42.192.103.123");
int ret = connect(sockfd, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
if(ret < 0)
{
perror("connect");
return -1;
}
while(1)
{
char buf[1024] = {0};
strcpy(buf, "i am client");
ret = send(sockfd, buf, strlen(buf), 0);
if(ret < 0)
{
perror("send");
return -1;
}
memset(buf, '\0', sizeof(buf));
ret = recv(sockfd, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("peer shutdown\n");
close(sockfd);
return 0;
}
printf("svr say: %s\n", buf);
}
close(sockfd);
return 0;
}
2.3 服务端程序
创建套接字,绑定地址信息,监听,获取连接,接收数据,发送数据,关闭套接字。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口重用
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(19999);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret < 0)
{
perror("listen");
return -1;
}
// 没有新连接时,该接口会阻塞
int newsockfd = accept(sockfd, NULL, NULL);
if(newsockfd < 0)
{
perror("accept");
return -1;
}
while(1)
{
char buf[1024] = {0};
ret = recv(newsockfd, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("peer shutdown\n");// 对端关闭
close(newsockfd);
return 0;
}
printf("cli say: %s\n", buf);
memset(buf, '\0', sizeof(buf));
const char* str = "i am server"; // str是临时变量
strncpy(buf, str, strlen(str)); // 不会越界
sleep(2);
ret = send(newsockfd, buf, strlen(buf), 0);
if(ret < 0)
{
perror("send");
return -1;
}
}
close(newsockfd);
close(sockfd);
return 0;
}
2.4 测试多个连接的情况
再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信。
分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求。
我们当前的这个TCP, 只能处理一个连接, 这是不科学的。
3. TCP多进程版本
通过每个请求, 创建子进程的方式来支持多连接。
// tcp_process_server.cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(19999);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret < 0)
{
perror("listen");
return -1;
}
while(1)
{
int newsockfd = accept(sockfd, NULL, NULL);
if(newsockfd < 0)
{
perror("accept");
return -1;
}
pid_t pid = fork(); // 创建子进程
if(pid < 0)
{
perror("pid");
close(newsockfd);
close(sockfd);
return -1;
}
else if(pid == 0)
{
//child
close(sockfd);
while(1)
{
char buf[1024] = {0};
ret = recv(newsockfd, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
return -1;
}
else if(ret == 0)
{
printf("peer shutdown\n");
close(newsockfd);
return 0;
}
printf("cli say: %s\n", buf);
memset(buf, '\0', sizeof(buf));
const char* str = "i am server";
strncpy(buf, str, strlen(str));
sleep(2);
ret = send(newsockfd, buf, strlen(buf), 0);
if(ret < 0)
{
perror("send");
return -1;
}
}
close(newsockfd);
}
else
{
//father
close(newsockfd);
}
}
close(sockfd);
return 0;
}
4. TCP多线程版本
通过每个请求, 创建一个线程的方式来支持多连接。
//tcp_thread_server.cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
// 从堆上开辟newsockfd_结构体
struct NewConnectSockFd
{
int newsockfd_;
};
// 工作线程入口函数
void* TestTcpStart(void* arg)
{
pthread_detach(pthread_self()); // 分离自己
struct NewConnectSockFd* nf = (struct NewConnectSockFd*)arg;
while(1)
{
char buf[1024] = {0};
int ret = recv(nf->newsockfd_, buf, sizeof(buf) - 1, 0);
if(ret < 0)
{
perror("recv");
close(nf->newsockfd_);
delete nf;
return NULL;
}
else if(ret == 0)
{
printf("peer shutdown\n");
close(nf->newsockfd_);
delete nf;
return NULL;
}
printf("cli say: %s\n", buf);
memset(buf, '\0', sizeof(buf));
const char* str = "i am server";
strncpy(buf, str, strlen(str));
sleep(2);
ret = send(nf->newsockfd_, buf, strlen(buf), 0);
if(ret < 0)
{
perror("send");
close(nf->newsockfd_);
delete nf;
return NULL;
}
}
delete nf; // 在所有退出的地方释放
return NULL;
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(19999);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 5);
if(ret < 0)
{
perror("listen");
return -1;
}
while(1)
{
// 主线程
int newsockfd = accept(sockfd, NULL, NULL);
if(newsockfd < 0)
{
perror("accept");
return -1;
}
struct NewConnectSockFd* ncsf = new NewConnectSockFd();
ncsf->newsockfd_ = newsockfd;// 刚接收回来的赋值给ncsf
pthread_t tid; //线程标识符
// 创建工作线程
ret = pthread_create(&tid, NULL, TestTcpStart, (void*)ncsf); // 将结构体传入
if(ret < 0)
{
delete ncsf;
perror("pthread_create");
return -1;
}
}
close(sockfd);
return 0;
}