首先我们先选择一个端口号用于 TCP 或 UDP 网络通信。如果你运行一个服务或应用程序,监听端口就是通过该端口接收来自客户端的请求。
这里我们选择2048
先在ubuntu系统中输入netstat -anop | grep 2048
,会显示以下信息
这代表此时2048
端口号没有被占用
我们在ubuntu系统中用c编写以下程序
c
#include <stdio.h> // 标准输入输出库,用于打印和输入
#include <sys/socket.h> // 套接字相关函数
#include <netinet/in.h> // 定义了 IP 协议族相关的常量和结构体
#include <string.h> // 字符串处理函数
#include <unistd.h> // 系统调用接口,包括 close 函数
#include <arpa/inet.h> // 提供了 IP 地址转换函数等
int main() {
// 创建一个 TCP 套接字,返回套接字描述符
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
// 配置服务器端的地址结构
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in)); // 初始化地址结构为零
serveraddr.sin_family = AF_INET; // 使用 IPv4 地址
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意 IP 地址
serveraddr.sin_port = htons(2048); // 绑定到端口 2048
// 将服务器端地址和端口绑定到套接字上
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
perror("bind");
return -1;
}
// 开始监听端口,最多允许 10 个连接排队
if (listen(sockfd, 10) == -1) {
perror("listen");
return -1;
}
// 等待客户端连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // 接受客户端连接
if (clientfd == -1) {
perror("accept");
return -1;
}
printf("Client connected\n");
// 接收客户端数据
char buffer[128] = {0}; // 缓冲区存放接收到的数据
int count = recv(clientfd, buffer, 128, 0); // 接收数据
if (count == -1) {
perror("recv");
close(clientfd);
return -1;
}
// 发送接收到的数据给客户端
send(clientfd, buffer, count, 0);
// 打印相关信息
printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);
// 等待用户输入后关闭客户端连接
getchar();
close(clientfd); // 关闭客户端套接字
return 0; // 程序正常结束
}
代码解释:
-
创建套接字:
cint sockfd = socket(AF_INET, SOCK_STREAM, 0);
使用
socket()
创建一个 TCP 套接字。AF_INET
表示使用 IPv4 地址族,SOCK_STREAM
表示创建一个流式套接字(TCP)。 -
初始化服务器地址结构:
cstruct sockaddr_in serveraddr; memset(&serveraddr, 0, sizeof(struct sockaddr_in)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(2048);
创建并初始化一个
sockaddr_in
结构体,配置服务器的地址和端口:sin_family
设置为AF_INET
表示 IPv4。sin_addr.s_addr
使用htonl(INADDR_ANY)
绑定到本地所有可用的网络接口。sin_port
设置为 2048,通过htons()
进行主机字节序到网络字节序的转换。
-
绑定套接字到地址:
cif (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) { perror("bind"); return -1; }
将套接字与指定的 IP 地址和端口绑定。
bind()
用于将套接字与服务器地址结构绑定。 -
监听客户端连接:
cif (listen(sockfd, 10) == -1) { perror("listen"); return -1; }
启动监听,参数
10
表示允许最多 10 个连接排队。 -
接受客户端连接:
cint clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
accept()
阻塞等待客户端连接,接受连接请求并返回一个新的套接字clientfd
用于与客户端通信。 -
接收和发送数据:
cint count = recv(clientfd, buffer, 128, 0); send(clientfd, buffer, count, 0);
recv()
从客户端接收数据,存储到buffer
中,最多接收 128 字节。send()
将接收到的数据重新发送给客户端。
-
关闭连接:
cclose(clientfd);
在交互结束后,关闭客户端连接。
编写完成后我们进行编译和运行
此时程序处于运行中
我们在系统再运行一次netstat -anop | grep 2048
显示
可以看到此时2048
端口处于监听状态
这时我们打开网络调试助手
- 协议类型选
TCP Client
- 远程主机地址就是虚拟机里ubuntu系统的ip地址
- 远程主机端口就是刚刚的
2048
点击连接
c程序这里也显示连接到了
再试一次netstat -anop | grep 2048
可以看到显示有一个客户端连接到了
这时我们在网络调试助手发送消息
c程序也接受到了消息并返回了相应的信息
但这个c程序创建的服务器是单线程的,可以使用更多的客户端进行连接,但是不能发送消息,而解决方法就是创建一个多线程的服务器,代码如下:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 2048 // 服务器监听的端口号
#define MAX_CLIENTS 10 // 最大客户端连接排队数
// 客户端处理函数,每个客户端连接会启动一个线程来执行此函数
void* handle_client(void* arg) {
int clientfd = *((int*)arg); // 获取客户端的套接字描述符
char buffer[128]; // 缓冲区,用于接收和发送数据
int count;
// 接收客户端发送的数据
count = recv(clientfd, buffer, sizeof(buffer), 0);
if (count == -1) {
perror("recv"); // 如果接收出错,输出错误信息并关闭连接
close(clientfd);
pthread_exit(NULL); // 结束线程
}
// 将接收到的数据发送回客户端(回显功能)
send(clientfd, buffer, count, 0);
printf("Received and sent back data: %s\n", buffer); // 打印接收到的数据
// 关闭与客户端的连接
close(clientfd);
pthread_exit(NULL); // 结束线程
}
int main() {
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket"); // 如果创建套接字失败,输出错误信息并返回
return -1;
}
// 配置服务器地址信息
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr)); // 清零结构体
serveraddr.sin_family = AF_INET; // 使用 IPv4 地址族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址
serveraddr.sin_port = htons(PORT); // 设置服务器监听端口
// 绑定套接字到指定的地址和端口
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
perror("bind"); // 如果绑定失败,输出错误信息并返回
close(sockfd);
return -1;
}
// 开始监听端口,最多允许 10 个连接排队
if (listen(sockfd, MAX_CLIENTS) == -1) {
perror("listen"); // 如果监听失败,输出错误信息并返回
close(sockfd);
return -1;
}
printf("Server listening on port %d...\n", PORT); // 打印服务器监听信息
// 变量定义:用于存储客户端地址信息和客户端套接字描述符
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd;
pthread_t thread_id; // 线程标识符
while (1) {
// 接受客户端连接请求
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd == -1) {
perror("accept"); // 如果接受连接失败,输出错误信息并跳过当前循环
continue;
}
// 打印客户端的 IP 地址和端口号
printf("New client connected from %s:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 创建新的线程来处理该客户端的请求
if (pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd) != 0) {
perror("pthread_create"); // 如果线程创建失败,输出错误信息并关闭客户端连接
close(clientfd);
continue;
}
// 将线程分离,线程结束时自动回收资源
pthread_detach(thread_id);
}
// 关闭服务器套接字,退出程序
close(sockfd);
return 0;
}
1. 包含头文件
c
#include <stdio.h> // 标准输入输出库,用于打印和输入
#include <stdlib.h> // 用于内存分配、退出程序等
#include <string.h> // 字符串操作函数
#include <unistd.h> // 系统调用接口,包含 close 函数等
#include <pthread.h> // 用于线程操作的库
#include <sys/socket.h> // 套接字相关函数
#include <netinet/in.h> // 定义了 IP 协议族相关的常量和结构体
#include <arpa/inet.h> // 提供了 IP 地址转换函数等
这些是程序所需要的头文件。它们提供了处理套接字、网络通信、线程管理等功能所需的函数和常量。
stdio.h
:标准输入输出库,提供printf
,perror
,scanf
等函数。stdlib.h
:提供动态内存分配(如malloc
)和程序退出(如exit
)等功能。string.h
:提供字符串处理函数(如memset
,strlen
)。unistd.h
:系统调用接口,提供诸如close
等系统调用。pthread.h
:线程管理库,提供创建、管理、同步线程的功能。sys/socket.h
和netinet/in.h
:提供创建和操作套接字的功能。arpa/inet.h
:提供 IP 地址相关的函数,如inet_ntoa
用于将in_addr
转换为可读的 IP 地址。
2. 定义常量
c
#define PORT 2048 // 服务器监听的端口号
#define MAX_CLIENTS 10 // 最大客户端连接排队数
PORT
:定义了服务器监听的端口号,设置为 2048。MAX_CLIENTS
:服务器在等待连接时,最多允许 10 个客户端请求排队。
3. handle_client
函数
c
void* handle_client(void* arg) {
int clientfd = *((int*)arg); // 获取客户端的套接字描述符
char buffer[128]; // 缓冲区,用于接收和发送数据
int count;
// 接收客户端发送的数据
count = recv(clientfd, buffer, sizeof(buffer), 0);
if (count == -1) {
perror("recv"); // 如果接收出错,输出错误信息并关闭连接
close(clientfd);
pthread_exit(NULL); // 结束线程
}
// 将接收到的数据发送回客户端(回显功能)
send(clientfd, buffer, count, 0);
printf("Received and sent back data: %s\n", buffer); // 打印接收到的数据
// 关闭与客户端的连接
close(clientfd);
pthread_exit(NULL); // 结束线程
}
-
参数说明:
arg
是一个void*
类型的指针,指向传递给线程的参数。在这个例子中,传递的是客户端套接字的描述符。
-
接收客户端数据:
ccount = recv(clientfd, buffer, sizeof(buffer), 0);
recv()
函数用来从客户端读取数据。它将客户端发送的数据存储到buffer
中,最多读取sizeof(buffer)
字节的数据。- 如果读取出错,
recv()
会返回-1
,程序会输出错误信息并关闭客户端连接。
-
回显数据:
csend(clientfd, buffer, count, 0);
send()
用于将从客户端接收到的数据发送回去。这里通过回显功能简单地将收到的数据重新发送给客户端。
-
关闭连接并结束线程:
cclose(clientfd); pthread_exit(NULL);
- 使用
close(clientfd)
关闭客户端连接。 pthread_exit(NULL)
结束线程的执行。
- 使用
4. main
函数
c
int main() {
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket"); // 如果创建套接字失败,输出错误信息并返回
return -1;
}
// 配置服务器地址信息
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr)); // 清零结构体
serveraddr.sin_family = AF_INET; // 使用 IPv4 地址族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址
serveraddr.sin_port = htons(PORT); // 设置服务器监听端口
// 绑定套接字到指定的地址和端口
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
perror("bind"); // 如果绑定失败,输出错误信息并返回
close(sockfd);
return -1;
}
// 开始监听端口,最多允许 10 个连接排队
if (listen(sockfd, MAX_CLIENTS) == -1) {
perror("listen"); // 如果监听失败,输出错误信息并返回
close(sockfd);
return -1;
}
printf("Server listening on port %d...\n", PORT); // 打印服务器监听信息
// 变量定义:用于存储客户端地址信息和客户端套接字描述符
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd;
pthread_t thread_id; // 线程标识符
while (1) {
// 接受客户端连接请求
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd == -1) {
perror("accept"); // 如果接受连接失败,输出错误信息并跳过当前循环
continue;
}
// 打印客户端的 IP 地址和端口号
printf("New client connected from %s:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 创建新的线程来处理该客户端的请求
if (pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd) != 0) {
perror("pthread_create"); // 如果线程创建失败,输出错误信息并关闭客户端连接
close(clientfd);
continue;
}
// 将线程分离,线程结束时自动回收资源
pthread_detach(thread_id);
}
// 关闭服务器套接字,退出程序
close(sockfd);
return 0;
}
1. 创建服务器套接字:
c
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
通过
socket()
创建一个 TCP 套接字。AF_INET
表示使用 IPv4 地址族,SOCK_STREAM
表示使用 TCP 协议(流式套接字)。 -
如果套接字创建失败,返回
-1
,程序通过perror()
输出错误信息并退出。
2. 配置服务器地址:
c
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr)); // 清零结构体
serveraddr.sin_family = AF_INET; // 使用 IPv4 地址族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的网卡地址
serveraddr.sin_port = htons(PORT); // 设置服务器监听端口
serveraddr
是一个sockaddr_in
结构体,用于保存服务器的地址信息。sin_family
设置为AF_INET
,表示 IPv4。sin_addr.s_addr = htonl(INADDR_ANY)
,表示将服务器绑定到所有可用的网卡地址。sin_port = htons(PORT)
,表示将服务器端口设置为预定的PORT
(2048)。
3. 绑定套接字到指定的地址和端口:
c
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
bind()
函数将服务器的套接字sockfd
与serveraddr
中存储的地址和端口绑定。
4. 开始监听端口:
c
listen(sockfd, MAX_CLIENTS);
listen()
函数使套接字进入监听模式,MAX_CLIENTS
定义了最多可以排队等待的客户端连接数。
5. 接受客户端连接:
c
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
accept()
函数接受一个客户端连接请求,返回一个新的套接字clientfd
用于与客户端进行通信。客户端的地址信息保存在 `
clientaddr` 中。
6. 创建线程处理客户端请求:
c
pthread_create(&thread_id, NULL, handle_client, (void*)&clientfd);
- 使用
pthread_create()
创建一个新线程来处理客户端请求。 handle_client
是线程运行的函数,参数传入客户端套接字clientfd
。
7. 线程分离:
c
pthread_detach(thread_id);
pthread_detach()
将线程设置为分离状态。线程结束后系统会自动回收资源,无需手动join
。
8. 关闭服务器套接字:
c
close(sockfd);
- 程序退出前,关闭服务器的监听套接字
sockfd
。
总结:
- 该代码实现了一个多线程的 TCP 服务器,能够同时处理多个客户端连接。
- 每当有一个新的客户端连接,服务器创建一个新的线程来处理该连接,避免了串行处理多个客户端带来的延迟。