[Linux 高并发服务器]TCP通信流程
文章概述
该文章为牛客C++项目课程:Linux高并发服务器的个人笔记,记录了使用socket来实现TCP通信的相关流程
作者信息
NEFU 2020级 zsl
ID:fishingrod/鱼竿钓鱼干
欢迎各位引用此博客,引用时在显眼位置放置原文链接和作者基本信息
参考资料
感谢前辈们留下的优秀资料,从中学到很多,冒昧引用,如有冒犯可以私信或者在评论区下方指出
正文部分
TCP通信流程
摘自牛客网课程的pdf
TCP通信服务器端
1. 创建一个用于监听的socket
使用socket
函数创建用于监听的套接字
int socket(int domain, int type, int protocol)
-domain:协议族
AF_INET:IPV4
AF_INET6:IPV6
AF_UNIX,AF_LOCAL:本地套接字用于进程间通信
-type:使用的协议类型
SOCK_STREAM:流式协议
SOCK_DGRAM:报式协议
-protocol:具体协议,一般写0
SOCK_STREAM:流式协议默认tcp
SOCK_DGRAM:报式协议默认udp
-返回值:
成功返回文件描述符
失败返回-1
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
2. 将这个监听文件描述符和本地的IP和端口绑定
绑定之前需要先创建和初始化一个专用socket地址struct sockaddr_in
存放本地IP和端口
sockaddr_in结构体
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)
创建和初始化
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
我们一般使用的IP地址表示方法为点分十进制表示法(一个字符串),而sockaddr_in
中存储的IP地址为一般为整形的网络字节序形式,因此我们可以使用inet_pton
函数来实现点分十进制IP地址字符床与网络字节序整数的转换。
当让我们可以直接给IP地址复制INADDR_ANY
或者0.0.0.0
二者的值是相同的,其意义是让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求
绑定
我们使用bind
函数来实现绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
// 2.绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
tip:注意对&saddr进行强制类型转换(struct sockaddr *)&saddr
早期网络编程函数使用通用socket地址struct sockaddr
结构体,为了向前兼容,现在sockaddr
退化成了void *
,也就是说这片内存区域的使用和解释方法取决于谁去用和怎么用。
专用socket地址类型变量在实际使用时都需要转化为通用socket地址类型socketaddr,因此在具体使用这些接口的时候必须指定具体的指针类型
3.设置监听,监听的fd开始工作
我们使用listen
函数来进行监听,listen
创建一个监听队列来接收客户端的连接,backlog参数就是设置监听队列的长度一般为5,如果设置为SOMAXCONN
那么就由系统来决定,一般是比较大的。
另外,如果监听队列满了那么客户端会收到 ECONNREFUSED
错误
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
4.阻塞等待客户端发起链接
我们使用accept
函数接收客户端发起的连接,并记录下来
因为需要记录因此,我们还需要一个struct socketaddr
来存储客户端的信息
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
如果我们要查看客户端信息,那么最好使用inet_ntop
将存储的网络字节序转化为点分十进制表示法,用ntohs
将端口从网络字节序转化为主机字节序
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
5.通信收发数据
还是那句话,Linux系统中万物皆是文件,socket也不例外。
在文件描述符后就可以使用read
和write
直接进行读写操作了
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data : %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("clinet closed...");
break;
}
char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
6.通信结束断开链接
当通信结束断开连接的时候,我们需要关闭监听的文件描述符和客户端的文件描述符
// 关闭文件描述符
close(cfd);
close(lfd);
最终代码
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data : %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("clinet closed...");
break;
}
char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
TCP通信客户端
客户端比服务端就简单许多了
1.创建一个用于通信的套接字
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
2.连接服务器,指定连接服务器的IP和端口
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
3.连接成功收发数据
// 3. 通信
char recvBuf[1024] = {0};
while(1) {
char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
}
4.通信结束,断开连接
5.最终代码
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
while(1) {
char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
运行结果
通过ifconfig查看ip地址,做适当修改,在本机进行socket测试