21. TCP网络编程
一、TCP协议简介
1.1、什么是TCP协议
TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。 TCP 协议则是建立在 IP 协议之上的。它旨在提供可靠的端到端通信,在发送数据之前,需要在两个通信端口之间建立连接。
TCP 协议会通过 3 次握手建立可靠连接。然后需要对每个 IP 包进行编号,确保对方按顺序收到,如果包丢了,就自动重发。一个 TCP 报文来了以后,到底是交给那个程序,就需要端口号来区分。每个网络程序都向操作系统申请一个端口号。这样,两个进程在两台计算机之间建立网络连接就需要各自的 IP 地址和各自的端口号。一个进程也可以能同时与多个计算机建立连接,因此它会申请多个端口。
TCP 每次发送一个数据包,对方都要进行确认,保证数据准确到达对象(不需要应用程序来做这件事,操作系统会自动来做);
TCP 通信的过程中,可以想象成这个虚拟的通道正在占用,不允许其它人来发送结束;
TCP 只需要建立一次连接,之后只需要发送数据,而不需要填写对象的 IP 和 PORT;
1.2、三次握手
TCP 是稳定的传输方式,在接收、发送之前,双发需要建立一个虚拟的通道,这个过程称为 3 次握手。3 次握手的流程如下:
- 客户端调用
connect()
时发送一个带有标记的数据包,我们把建立连接时的第 1 次数据叫做 SYN,其中有 1 个数字; - 服务器接收到这个 SYN 数据包,提取出数字,然后 +1,回送给客户端。这个数据包中有 2 部分:SYN + ACK;
- ACK 是对接收到的数据的确认;
- SYN 表示要向客户端发送的数据;
- 当客户端接收到 SYN + ACK 数据包之后,提取数字,然后加 1,然后用 ACK 数据包回送给服务器;
当客户端调用
connect()
方法的时候,就有了 TCP 的 3 次握手,目的是让双方都分配一些资源(内存等)为将来进行网络通信时做准备;服务器会阻塞到
accept()
方法这里,直到客户端发起连接,即 3 次握手完成之后,accept()
才会解阻塞,并且accept()
返回一个新的套接字还有刚刚连接成功的 IP 和 PORT;
1.3、四次挥手
为了释放资源,所以双方需要协商怎样关闭这个虚拟的通道,这就是 4 次挥手。4 次挥手的过程如下:
- 客户端先发送一个数据包,这里有 1 个数字,4 次挥手开始的第 1 次数据包称为 FIN;
- 服务器接收到 FIN 数据包,然后将数字提取出来,然后 +1,通过 ACK 数据包发送给客户端;
- 此时服务器的
recv()
会解阻塞,并且返回的数据长度为 0; - 如果服务器对已经建立的套接字调用
close()
,那么就会有下面的 2 次挥手;
- 此时服务器的
- 服务器发送一个数据,这里有 1 个数字,这个包类型是 FIN;
- 当客户端接收服务器的 FIN 时,提取出数字,然后 +1,然后用 ACK 数据包回送给服务器;
当客户端调用
close()
方法时,操作系统会发起 TCP 的 4 次挥手;当服务器调用
close()
方法时,才会发送第 3 次挥手数据;
二、TCP编程的API
2.1、创建套接字
我们可以使用 socket()
函数 创建一个套接字。在 TCP 连接中,客户端和服务端都需要创建对应的套接字。
/**
* @brief 创建一个套接字
*
* @param __domain 指定要创建套接字的通信域
* @param __type 指定要创建的套接字类型
* @param __protocol 指定要与socket一起使用的特定协议
* @return int 成功返回文件描述符,失败返回-1
*/
int socket(int __domain, int __type, int __protocol);
参数 __domain
用来 指定要创建套接字的通信域。这里我们使用 AF_INET
表示 使用 IPv4 互联网协议,如果要 使用 IPv6 协议,我们可以指定为 AF_INET6
。
参数 __type
用来 指定要创建的套接字类型。如果我们要 使用 TCP 协议,可以指定为 SOCK_STREAM
。如果我们要 使用 UDP 协议,可以指定为 SOCK_DGRAM
。
参数 __protocol
用来 指定要与 socket 一起使用的特定协议。如果我们将指定协议设置为 0,会导致 socket() 函数的使用会自动适用于所请求的 socket 类型的未指定的默认协议。
2.2、绑定地址和端口号
创建完套接字之后,客户端和服务端都使用 bind()
函数 绑定地址和端口号。
/**
* @brief 绑定地址和端口号
*
* @param __fd 套接字文件描述符
* @param __addr 指定的地址
* @param __len __addr指向地址结构的大小(以字节为单位)
* @return int 成功返回0,失败返回-1
*/
int bind(int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
当我们使用 socket()
函数创建套接字时,它存在于一个名称空间(地址族)中,但没有为其分配地址。bind()
函数将有 __addr
指定的地址分配给文件描述符 __fd
所一i你用的套接字。__len
指定了 __addr
指向的地址结构的大小(以字节为单位)。
在网络编程中,地址族(Address Family)指定了套接字使用网络协议类型以及地址的格式。简而言之,地址族决定了网络通信的范围和方式。每种地址族都支持特定类型的通信协议和地址格式。
2.3、服务端监听连接
在 TCP 协议中,服务端需要使用 listen()
函数 监听连接。
/**
* @brief 服务端监听连接
*
* @param __fd 套接字文件描述符
* @param __n 指定处于等待状态的连接请求数量
* @return int 成功返回0,失败返回-1
*/
int listen(int __fd, int __n);
listen()
是 Linux 提供的用于 TCP 网络编程的系统调用,作用是让一个套接字进入监听状态,准备接受连接请求。当 listen()
函数被调用之后,__fd
指定的套接字会从一个主动套接字转变为一个被动套接字,表明它将被用来接受进来的请求连接,而不是主动发起连接。accept()
函数随后用于响应来连接请求。
2.4、服务端接受客户端连接
在 TCP 协议中,服务端需要使用 accept()
函数来 接受客户端的连接。
/**
* @brief 服务端接受客户端连接
*
* @param __fd 套接字文件描述符
* @param __addr 保存连接的客户端的socket地址
* @param __addr_len 保存连接的客户端socket地址长度
* @return int 成功返回一个新的套接字文件描述符,失败返回-1
*/
int accept(int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
accept()
函数从监听套接字的文件描述符的待处理连接队列中提取出第一个连接请求,创建一个新的连接套接字,并返回指向该套接字的新的文件描述符。
2.5、客户端请求连接
在 TCP 中,客户端需要使用 connect()
函数来 连接服务器。
/**
* @brief 客户端连接服务器
*
* @param __fd 套接字文件描述符
* @param __addr 服务端的socket地址
* @param __len 服务端的socket地址长度
* @return int 成功返回0,失败返回-1
*/
int connect(int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
2.6、发送数据
在客户端连接上服务器之后,客户端和服务端都可以使用 send()
函数向对方 发送数据。
/**
* @brief 发送数据
*
* @param __fd 套接字文件描述符
* @param __buf 发送缓冲区
* @param __n 要发送的数据长度
* @param __flags 发送标志,对于大部分应用,可以直接设置为0
* @return ssize_t 返回实际发送的数据长度,出错返回-1
*/
ssize_t send(int __fd, const void *__buf, size_t __n, int __flags);
2.7、接收数据
当一端发送数据之后,另一端可以使用 recv()
函数来 接收数据。
/**
* @brief 接收数据
*
* @param __fd 套接字文件描述符
* @param __buf 接收缓冲区
* @param __n 缓冲区可以接收的最大字节数
* @param __flags 接收标志,对于大多数应用,可以直接设置为0
* @return ssize_t 返回实际接收的字节数,如果发生错误,则返回-1
*/
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags);
2.8、关闭连接
我们可以使用 shutdown()
函数来 关闭套接字的连接。
/**
* @brief 关闭套接字的一部分或者全部连接
*
* @param __fd 套接字文件描述符
* @param __how 指定关闭的类型
* @return int 成功返回0,失败返回-1
*/
int shutdown(int __fd, int __how);
参数 __how
用来 指定关闭的连接,其取值如下:
SHUT_RD
:关闭读。之后,该套接字不再接收数据。任何当前阻塞再 recv() 调用上的操作都返回 0,表示连接的另一端已经关闭。SHUT_WR
:关闭写。之后,试图通过该套接字发送数据将导致错误。如果使用此选项,TCP 连接将发送一个 FIN 包给连接的对端,表示此方向上的数据传输已经完成。此时对端的 recv() 调用将接收到 0。SHUT_RDWR
:关闭读写。同时关闭套接字的读取和写入部分,等同于分别调用SHUT_RD
和SHUT_WR
。之后,该套接字既不能接收数据也不能发送数据。
我们还可以使用 close()
函数直接 关闭套接字的所有连接。
/**
* @brief 关闭套接字全部连接
*
* @param __fd 套接字文件描述符
* @return int 成功返回0,失败返回-1
*/
int close(int __fd);
2.9、网络字节序和主机字节序的转换
字节序 指的是多字节数据在内存中的存储顺序,主要有两种字节序。
- 大端字节序(Big Endian):高位字节存储在内存的低地址处,低位字节存储在高位地址处。这种字节序也称为 网络字节序。
- 小端字节序(Little Endian):低位字节存储在内存的低地址处,高位字节存储在高地址处。这种字节序也称为 主机字节序。
我们可以使用 htonl()
函数或 htons()
函数 将主机字节序转换为网络字节序。
uint32_t htonl(uint32_t __hostlong);
uint16_t htons(uint16_t __hostshort);
如果我们想要 从网络字节序转换为主机字节序,可以使用 ntohl()
函数或 htohs()
函数。
uint32_t ntohl(uint32_t __netlong);
uint16_t ntohs(uint16_t __netshort);
上面的函数只是将字节转换好了,后面我们还需要手动填入 struct in_addr
结构体中。此时我们可以使用 inet_aton()
函数直接将 IPv4 点分十进制表示法的主机地址的字符串转换为二进制形式(以网络字节序),并将其存储在 __inp
指向的结构体中。该函数转换成功返回 1,失败则返回 0。
int inet_aton(const char *__cp, struct in_addr *__inp);
inet_aton()
函数只能用于 IP 协议,如果我们想要使用其它协议,可以使用 inet_pton()
函数。
/**
* @brief 将字符串转换为 sockadd_in 结构体格式
*
* @param __af 指定协议类型, AF_INET 表示 IPv4 协议,AF_INET6 表示 IPv6 协议
* @param __cp 包含IP地址字符串的字符数组,
* 如果是 IPv4 协议,格式为点分十进制(如 "192.168.1.1"),
* 如果是 IPv6 协议,格式为冒号分隔的16进制数(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
* @param __buf 指向一个缓冲区,
* 如果是 IPv4 协议,该缓冲区应该是一个 struct in_addr 结构体指针,
* 如果是 IPv6 协议,该缓冲区应该是一个 struct in6_addr 结构体指针
* @return int 成功转换返回0,输入地址错误返回1,发生错误返回-1
*/
int inet_pton(int __af, const char *__restrict __cp, void *__restrict __buf);
如果我们想要返过来,可以使用 inet_ntoa()
函数。
char *inet_ntoa(struct in_addr __in);
三、TCP网络编程
3.1、创建TCP服务器
创建 TCP 服务器的伪代码如下:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
#define TCP_MAX_CONNECT_COUNT 5
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
char data[1024] = {0};
ssize_t length = 0;
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
server_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定地址
bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address));
// 监听连接
listen(server_socket_fd, TCP_MAX_CONNECT_COUNT);
while (1)
{
// 接收客户端连接
socklen_t client_address_length = sizeof(client_address);
client_socket_fd = accept(server_socket_fd, (struct sockaddr *)&client_address, &client_address_length);
// 通信循环
while (1)
{
// 服务端接收客户端发送的数据
length = recv(client_socket_fd, data, sizeof(data), 0);
// 服务端向客户端返回的数据
send(client_socket_fd, data, strlen(data), 0);
}
// 关闭客户端
close(client_socket_fd);
}
// 关闭服务端
close(server_socket_fd);
return 0;
}
所有套接字都是通过使用 socket() 函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。特别地,TCP 服务器必须监听(传入)的连接。
调用 accept() 方法之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept() 是阻塞的,这意味着指定将被暂停,直到一个连接到达。一旦服务器接收一个连接,就会返回(利用 accept())一个独立的客户端套接字,用来与即将到来的消息交换。当一个传入的请求到达时,服务器会创建一个新的通信接口来直接与客户端进行通信,再次空出主要的端口,以使其接收新的客户端连接。
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直达连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。
我们新建一个【tcp】文件夹,然后在该文件夹下新建一个 tcp_server.c 文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
#define TCP_MAX_CONNECT_COUNT 5
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
socklen_t client_address_length = sizeof(client_address);
time_t raw_time = 0;
struct tm *time_info= NULL;
char data[1024] = {0};
ssize_t length = 0;
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
if ((server_socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("create server socket failed.");
exit(EXIT_FAILURE);
}
// 绑定地址
if (bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
perror("bind server address failed.");
exit(EXIT_FAILURE);
}
// 进入监听状态
if (listen(server_socket_fd, TCP_MAX_CONNECT_COUNT) == -1)
{
perror("server listen failed.");
exit(EXIT_FAILURE);
}
while (1)
{
printf("waiting for connection.\n");
// 获取客户端的连接
// 这里返回的socket文件描述符才是能和客户端收发消息的文件描述符
// 如果调用accept()方法之后,没有客户端连接,这里会挂起等待
if ((client_socket_fd = accept(server_socket_fd, (struct sockaddr *)&client_address, &client_address_length)) == -1)
{
perror("server accept failed.");
exit(EXIT_FAILURE);
}
printf("establish a connection with the client (%s:%d).\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
while (1)
{
// 服务端接收消息,单次最大接收为1024个字节
memset(data, 0, sizeof(data));
length = recv(client_socket_fd, data, sizeof(data), 0);
// 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
if (length == 0)
{
printf("client (%s:%d) requested to disconnect.\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
break;
}
data[length] = '\0';
printf("receiving the data sent by the client (%s:%d): \n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
printf("%s\n", data);
// 服务端返回数据
time(&raw_time);
time_info = localtime(&raw_time);
char date[128] = {0};
sprintf(date,
"(%d-%d-%d %d:%d:%d)\n",
time_info->tm_year + 1900, time_info->tm_mon + 1, time_info->tm_mday,
time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
int date_length = strlen(date);
for (int i = length; i >= 0; i--)
{
data[i + date_length] = data[i];
}
memcpy(data, date, strlen(date));
send(client_socket_fd, data, strlen(data), 0);
}
// 关闭客户端
close(client_socket_fd);
}
// 关闭服务端
close(server_socket_fd);
return 0;
}
在代码中,一个客户端连接关闭之后,服务器就会等到另一个客户端连接。
3.2、创建TCP客户端
创建 TCP 服务器的伪代码如下:
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define TCP_CLIENT_IP INADDR_ANY
#define TCP_CLIENT_PORT 8001
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in client_address = {0}, server_address = {0};
int socket_fd = 0;
char data[1024] = {0};
ssize_t length = 0;
// 填写客户端地址
client_address.sin_family = AF_INET; // 使用IPv4协议
client_address.sin_addr.s_addr = htonl(TCP_CLIENT_IP); // 填写客户端的IP地址为本地的IP地址
client_address.sin_port = htons(TCP_CLIENT_PORT); // 填写客户端的端口号
// 创建客户端socket
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定地址
bind(socket_fd, (struct sockaddr *)&client_address, sizeof(client_address));
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 客户端连接服务端
// 此时socket_fd就是客户端用于和服务器通信的文件描述符
// 可以使用这个文件描述符进行数据的发送(如send()函数)和接收(如recv()函数)等操作。
connect(socket_fd, (struct sockaddr *)&server_address, sizeof(server_address));
while (1)
{
// 客户端向服务端发送数据
send(socket_fd, data, strlen(data), 0);
// 客户端接收服务端返回的数据
recv(socket_fd, data, sizeof(data), 0);
}
// 关闭客户端
close(socket_fd);
return 0;
}
所有套接字都是利用 socket() 创建的。然而,一旦客户端拥有了一个套接字,它就可以利用套接字的 connect() 方法直接创建一个到服务器的连接。当连接建立之后,它就可以直接参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。
这里,我们在【tcp】文件夹下再新建一个 tcp_client.c 文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define TCP_CLIENT_IP INADDR_ANY
#define TCP_CLIENT_PORT 8001
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in client_address = {0}, server_address = {0};
int socket_fd = 0;
char data[1024] = {0};
ssize_t length = 0;
// 填写客户端地址
client_address.sin_family = AF_INET; // 使用IPv4协议
client_address.sin_addr.s_addr = htonl(TCP_CLIENT_IP); // 填写客户端的IP地址为本地的IP地址
client_address.sin_port = htons(TCP_CLIENT_PORT); // 填写客户端的端口号
// 创建客户端socket
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("create client socket failed.");
exit(EXIT_FAILURE);
}
// 绑定地址
if (bind(socket_fd, (struct sockaddr *)&client_address, sizeof(client_address)) == -1)
{
perror("bind client address failed.");
exit(EXIT_FAILURE);
}
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 客户端连接服务端
// 此时socket_fd就是客户端用于和服务器通信的文件描述符
// 可以使用这个文件描述符进行数据的发送(如send()函数)和接收(如recv()函数)等操作。
if (connect(socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
printf("client (%s:%d) connect server (%s:%d) failed.\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port),
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
exit(EXIT_FAILURE);
}
printf("client (%s:%d) connect server (%s:%d).\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port),
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
while (1)
{
memset(data, 0, sizeof(data));
printf("please enter the data to be send.\n");
fgets(data, sizeof(data), stdin);
if (data == NULL || data == "")
{
break;
}
if (data[0] == '\n')
{
continue;
}
// 客户端向服务端发送数据
send(socket_fd, data, strlen(data), 0);
memset(data, 0, sizeof(data));
// 客户端接收服务端返回的数据
recv(socket_fd, data, sizeof(data), 0);
printf("received data returned by the server (%s:%d):\n",
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
printf("%s\n", data);
}
// 关闭客户端
close(socket_fd);
return 0;
}
3.3、执行TCP服务器和客户端
如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动的等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说,首先启动服务器(在任何客户端试图连接之前)。
我们在终端中输入 gcc 命令编译文件,生成对应的可执行程序。
gcc tcp_server.c -o tcp_server
gcc tcp_client.c -o tcp_client
然后,我们在新建一个终端,在第一个终端中先运行服务端的可执行程序。
./tcp_server
然后,我们在第二个终端中先运行客户端的可执行程序。
./tcp_client
四、多线程并发TCP服务器
我们使用线程的方式实现并发 TCP 服务器。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
#define TCP_MAX_CONNECT_COUNT 5
struct client_t
{
struct sockaddr_in address;
int fd;
};
void *tcp_server(void *argv);
int main(void)
{
struct sockaddr_in server_address = {0}, client_address = {0};
int server_socket_fd = 0, client_socket_fd = 0;
socklen_t client_address_length = sizeof(client_address);
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 创建socket
if ((server_socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("create server socket failed.");
exit(EXIT_FAILURE);
}
// 绑定地址
if (bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
perror("bind server address failed.");
exit(EXIT_FAILURE);
}
// 进入监听状态
if (listen(server_socket_fd, TCP_MAX_CONNECT_COUNT) == -1)
{
perror("server listen failed.");
exit(EXIT_FAILURE);
}
while (1)
{
int client_socket_fd = 0;
pthread_t pid = 0;
printf("waiting for connection.\n");
// 获取客户端的连接
// 这里返回的socket文件描述符才是能和客户端收发消息的文件描述符
// 如果调用accept()方法之后,没有客户端连接,这里会挂起等待
if ((client_socket_fd = accept(server_socket_fd, (struct sockaddr *)&client_address, &client_address_length)) == -1)
{
perror("server accept failed.");
exit(EXIT_FAILURE);
}
printf("establish a connection with the client (%s:%d).\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
// 每一个客户端使用一个线程
struct client_t client = {client_address, client_socket_fd};
if (pthread_create(&pid, NULL, tcp_server, (void *)&client) < 0)
{
printf("create client (%s:%d) connect server thread failed.\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
exit(EXIT_FAILURE);
}
// 需要等待线程结束,但是不能挂起等待
pthread_detach(pid);
}
// 关闭服务端
close(server_socket_fd);
return 0;
}
void *tcp_server(void *argv)
{
struct client_t client = *(struct client_t *)argv;
int client_socket_fd = client.fd;
struct sockaddr_in client_address = client.address;
time_t raw_time = 0;
struct tm *time_info = NULL;
char data[1024] = {0};
ssize_t length = 0;
while (1)
{
// 服务端接收消息,单次最大接收为1024个字节
memset(data, 0, sizeof(data));
length = recv(client_socket_fd, data, sizeof(data), 0);
// 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
if (length == 0)
{
printf("client (%s:%d) requested to disconnect.\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
break;
}
data[length] = '\0';
printf("receiving the data sent by the client (%s:%d): \n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
printf("%s\n", data);
// 服务端返回数据
time(&raw_time);
time_info = localtime(&raw_time);
char date[128] = {0};
sprintf(date,
"(%d-%d-%d %d:%d:%d)\n",
time_info->tm_year + 1900, time_info->tm_mon + 1, time_info->tm_mday,
time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
int date_length = strlen(date);
for (int i = length; i >= 0; i--)
{
data[i + date_length] = data[i];
}
memcpy(data, date, strlen(date));
send(client_socket_fd, data, strlen(data), 0);
}
// 关闭客户端
close(client_socket_fd);
}
然后,我们修改 tcp_client.c 文件,删除客户端的端口号,让系统自动分配一个空闲的端口号给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define TCP_CLIENT_IP INADDR_ANY
#define TCP_SERVER_IP INADDR_ANY
#define TCP_SERVER_PORT 8000
int main(void)
{
struct sockaddr_in client_address = {0}, server_address = {0};
int socket_fd = 0;
char data[1024] = {0};
ssize_t length = 0;
// 创建客户端socket
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("create client socket failed.");
exit(EXIT_FAILURE);
}
// 客户端可以不绑定,系统会自动分配一个空闲的端口给客户端
// 填写服务端地址
server_address.sin_family = AF_INET; // 使用IPv4协议
server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP); // 填写服务端的IP地址为本地的IP地址
server_address.sin_port = htons(TCP_SERVER_PORT); // 填写服务端的端口号
// 客户端连接服务端
// 此时socket_fd就是客户端用于和服务器通信的文件描述符
// 可以使用这个文件描述符进行数据的发送(如send()函数)和接收(如recv()函数)等操作。
if (connect(socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
{
perror("client connect server failed.");
exit(EXIT_FAILURE);
}
printf("client (%s:%d) connect server (%s:%d)\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port),
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
while (1)
{
memset(data, 0, sizeof(data));
printf("please enter the data to be send.\n");
fgets(data, sizeof(data), stdin);
if (data == NULL || data == "")
{
break;
}
if (data[0] == '\n')
{
continue;
}
// 客户端向服务端发送数据
send(socket_fd, data, strlen(data), 0);
memset(data, 0, sizeof(data));
// 客户端接收服务端返回的数据
recv(socket_fd, data, sizeof(data), 0);
printf("received data returned by the server (%s:%d):\n",
inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
printf("%s\n", data);
}
// 关闭客户端
close(socket_fd);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2024-02-18 07. µCOS-Ⅲ的信号量