我们首先来看一个最简单的TCP服务端、客户端的例子,服务端在端口8080等待连接,客户端发起连接,连接成功后发送“Hello,Server”,然后关闭连接;服务端接收客户端的消息并打印,然后关闭连接。
服务端代码:
#include
#include
#include
#include
#include
#include
#include
#define PORT 8080 // 要监听的端口号
#define MAX_CONN 5 // 最大并发连接数
int main() {
int server_sock, client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
char buffer[1024];
// 创建一个套接字
if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "Failed to create socket." << std::endl;
return -1;
}
// 设置地址结构
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字到指定IP和端口
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) <
0) {
std::cerr << "Failed to bind socket." << std::endl;
close(server_sock);
return -1;
}
// 开始监听连接请求
if (listen(server_sock, MAX_CONN) < 0) {
std::cerr << "Failed to listen on socket." << std::endl;
close(server_sock);
return -1;
}
std::cout << "Server is listening on port " << PORT << std::endl;
while (true) {
// 接受新的连接请求
if ((client_sock = accept(server_sock, (struct sockaddr*)&client_addr,
&addr_len)) < 0) {
std::cerr << "Failed to accept connection." << std::endl;
continue;
}
std::cout << "Accepted connection from: " << inet_ntoa(client_addr.sin_addr)
<< ":" << ntohs(client_addr.sin_port) << std::endl;
// 接收客户端发送的数据
ssize_t n = recv(client_sock, buffer, sizeof(buffer), 0);
if (n < 0) {
std::cerr << "Failed to receive data from client." << std::endl;
close(client_sock);
continue;
}
buffer[n] = '\0'; // 添加字符串结束符
std::cout << "Received message: " << buffer << std::endl;
// 关闭客户端连接
close(client_sock);
}
// 关闭服务器套接字
close(server_sock);
return 0;
}
第10行,定义服务端要监听的端口;
第11行,这是listen函数第二个参数backlog的值,在 Linux 2.2 中,TCP 套接字的 backlog参数的行为发生了变化。现在它指定了完全建立的套接字等待被接受的队列长度,而不是未完成连接请求的数量。未完成套接字的队列最大长度可以使用
/proc/sys/net/ipv4/tcp_max_syn_backlog 来设置。当启用 syncookies (这里syncookies先挖一个坑,后面有机会会详细介绍)时,没有逻辑上的最大长度,并且此设置被忽略。
第20行,创建一个socket套接字,AF_INET表示使用网络协议IPv4版本,SOCK_STREAM表示提供有序、可靠、双向、基于连接的字节流。第三个参数默认写0。用TCP协议且IPv4时,就照着这样写。
第27行,设置网络协议为版本IPv4;
第28行,把端口的本地字节序改成网络字节序;这里为什么要转成网络字节序呢?在网络通信中,不同的计算机可能具有不同的字节序,例如大端字节序或小端字节序。字节序决定了一个多字节数值(如整数或浮点数)在内存中是如何存储的。在大端字节序中,高位字节(即最左边的字节,对应于最大的位权重)存储在最小的地址处;而在小端字节序中,低位字节存储在最小的地址处。为了避免混淆和确保跨平台通信的一致性,网络字节序被定义为大端字节序。这意味着无论发送或接收数据的计算机的本地字节序是什么,它们都需要在网络通信期间将数据转换为网络字节序。
第29行,INADDR_ANY表示绑定套接字到本机所有IP地址(0.0.0.0),因为一台机器可能有多个网卡,多个IP。如果需要绑定到特定的IP地址需要把IP地址字符串转成网络序的整型数字这样写:
server_addr.sin_addr.s_addr = inet_addr("XX.XX.XX.XX");
第32行,把刚才生成的套接字绑定到IP和端口。这里注意第二个参数做了类型转换,struct sockaddr_in*转换成struct sockaddr*,这是因为struct sockaddr这个结构体把ip和端口放在了一个变量里,不方便使用,所以新增了struct sockaddr_in,struct sockaddr_in把IP和端口分成了两个变量,方便使用。
第40行,服务端开始监听,即可以接受客户端的连接请求,参数值不再赘述。
第50行,接受完成3次握手的连接(存储在全连接队列里)请求,如果没有连接请求,accept将在此阻塞,因为默认建立的套接字文件描述符是阻塞的。接受成功后,客户端的IP和端口信息将会存储在client_addr变量里。
第56行和57行,以可读的形式打印客户端的IP和端口。
第60行,阻塞地读取客户端发送的信息,我们可以看到,这个时候我们读取的文件描述符是client_sock,不是之前的server_sock;client_sock默认也是阻塞的,程序会一直等待直到有数据或者出现错误,例如客户端断开。
第71行,主动断开与客户端的连接,等待下一个连接。
第75行,关闭监听的套接字,不再有端口监听。
由于篇幅原因,客户端的程序会在下一片文章介绍。我也会在后面的文章中继续介不断改进我们的代码直至一个可用的服务器端网络服务库或者框架。