提示:Linux环境下使用c++通过Socket套接字实现TCP协议通讯。本文源码可直接运行使用。
文章目录
前言
本文介绍了如何在Linux环境下,使用c++通过Socket套接字实现TCP协议通讯。对Socket套接字中需要使用的函数、传参及函数返回值等细节问题做出了详细的解释。另外需要提醒的是,在学习TCP/IP协议时应先了解Socket套接字、g++和Linux操作命令等基础知识。
一、Socket套接字详解
1.socket函数及参数返回值详解
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket < 0)
{
std::cout << "socket error" << std::endl;
return 1;
}
这行代码是用来创建一个基于 IPv4 地址族(AF_INET)和 TCP 协议(SOCK_STREAM)的套接字,用于接受客户端的连接请求并与之建立 TCP 连接。接下来还需要调用 bind() 绑定地址和端口、listen() 开始监听连接请求等操作,以完成服务器端的初始化工作。
socket()函数的返回值 serverSocket,用于表示服务器端的套接字,当serverSocket<0时,表示创建失败。
在使用 socket 函数创建套接字时,包括地址族(Address Family)、套接字类型(Socket Type)和协议(Protocol)三个参数。具体参数如下:
地址族(Address Family):
AF_INET:IPv4 地址族,用于指定使用 IPv4 地址。
AF_INET6:IPv6 地址族,用于指定使用 IPv6 地址。
AF_UNIX 或 AF_LOCAL:本地通信地址族,用于在同一台计算机上的进程间通信。
套接字类型(Socket Type):
SOCK_STREAM:面向连接的字节流套接字,提供可靠的、基于字节流的数据传输服务,对应 TCP 协议。
SOCK_DGRAM:无连接的数据报套接字,提供不可靠的数据传输服务,对应 UDP 协议。
SOCK_RAW:原始套接字,可以直接访问底层网络协议,适用于特定的网络操作需求。
可以使用 SOCK_RAW 套接字类型来实现对 TCP 协议的访问和处理,但需要注意以下几点:
使用 SOCK_RAW 套接字类型需要更高级别的权限,通常需要 root 权限或者具有CAP_NET_RAW 能力的用户才能够使用。并且开发者需要自行处理网络数据包的构建和解析,包括处理 IP 头部、TCP 头部和数据部分等内容,而不再依赖操作系统提供的传输层协议栈的功能。需要开发者自行处理更多底层网络协议的细节,因此具有更高的复杂性和风险。一般情况下,仅在特定需求下,如实现自定义的网络协议栈或进行底层网络数据包操作时才会选择使用 SOCK_RAW 套接字类型。
理论上可以使用 SOCK_RAW 套接字类型来实现对 TCP 协议的访问和处理,但在实际应用中,通常会使用更高层次的套接字类型(如 SOCK_STREAM)或者现有的网络编程库来简化开发和提高可移植性。
协议(Protocol):
通常设置为 0,表示根据地址族和套接字类型自动选择合适的协议。
对于 SOCK_STREAM 类型的套接字,通常会选择 IPPROTO_TCP(TCP 协议)。
对于 SOCK_DGRAM 类型的套接字,通常会选择 IPPROTO_UDP(UDP 协议)。
2.sockaddr_in结构体、inet_pton函数及参数返回值详解
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) < 0)
{
close(serverSocket);
std::cout << "inet_pton error" << std::endl;
return -1;
}
先要定义一个sockaddr_in结构体serverAddr,然后设置了该结构体的成员sin_family为AF_INET 表示使用 IPv4 地址,sin_port 为服务器的端口号,sin_addr 为服务器的 IP 地址。最后一行代码使用了 inet_pton 函数将点分十进制的 IP 地址转换为网络字节顺序的二进制形式,并存储到 serverAddr.sin_addr 中。
sockaddr_in 结构体是用于表示 IPv4 地址和端口的数据结构,通常用于网络编程中。该结构体定义如下:
struct sockaddr_in {
short int sin_family; // 地址族,一般设置为 AF_INET
unsigned short sin_port; // 端口号,网络字节顺序
struct in_addr sin_addr; // IPv4 地址结构体
char sin_zero[8]; // 未使用,填充使结构体大小与 sockaddr 相同
};
sin_family:地址族,一般设置为 AF_INET 表示使用 IPv4 地址。
sin_port:端口号,使用 unsigned short 类型表示,需要使用 htons() 函数将主机字节顺序转换为网络字节顺序。
sin_addr:struct in_addr 类型的结构体,用于表示 IPv4 地址。
sin_zero:填充字段,用于使整个结构体的大小与 struct sockaddr 相同,通常不使用。
3.bind函数及参数返回值详解
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
{
close(serverSocket);
std::cout << "bind error" << std::endl;
return -1;
}
serverAddr是一个sockaddr结构体类型的变量,存储了服务器的IP地址和端口号信息,bind函数用于将一个本地地址(&serverAddr)绑定到一个socket上。具体来说,是将serverSocket与serverAddr绑定在一起。返回值>0。
4.listen函数及参数返回值详解
if (listen(serverSocket, LISTEN_NUM) < 0)
{
close(serverSocket);
std::cout << "listen error" << std::endl;
return -1;
}
listen(serverSocket, LISTEN_NUM)用于告诉操作系统,serverSocket将被用作一个监听socket,并且指定允许同时连接的最大客户端数量为LISTEN_NUM。
在Socket编程中,一旦调用了listen函数,serverSocket就变成了一个监听socket,开始接受来自客户端的连接请求。LISTEN_NUM参数指定了操作系统允许在等待连接队列中排队的最大连接数。如果有更多的连接请求到达,它们将被拒绝或者等待直到有连接空闲为止。
需要注意的是,LISTEN_NUM的具体取值应根据实际需求进行设置,过小可能导致无法处理所有连接请求,过大则可能浪费资源。
5.accept函数及参数返回值详解
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize);
if (clientSocket < 0)
{
close(serverSocket);
std::cout << "accept error" << std::endl;
return -1;
}
accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize)用于接受客户端的连接请求,并返回一个新的socket来与客户端通信。需要注意的是新的socket也就是clientSocket 仍是服务端的套接字。
具体来说,当服务器监听serverSocket接收到一个客户端的连接请求时,accept函数会创建一个新的socket来处理与该客户端的通信。同时,clientAddr 是一个sockaddr结构体类型的变量,用于存储客户端的IP地址和端口号信息。clientAddrSize 则是一个变量,用来传递clientAddr 结构体的大小。
在调用accept函数后,如果成功接受了客户端的连接请求,将返回一个新的socket描述符也就是clientSocket ,通过这个socket描述符可以进行与客户端之间的通信。
6.connect函数及参数返回值详解
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
{
std::cout << "connect error" << std::endl;
close(clientSocket);
return -1;
}
connect函数是用在客户端上发起与服务器的连接请求。
具体来说,客户端socket clientSocket 将尝试连接到指定的服务器地址 serverAddr,该地址包含了服务器的IP地址和端口号信息。通过调用 connect 函数,客户端将尝试与指定的服务器建立连接。
在实际应用中,客户端调用 connect 函数后,如果连接成功建立,返回值通常为0;如果连接失败,返回值会有相应的错误代码,需要根据返回值来判断连接是否成功建立。需要注意的是,在使用 connect 函数前,通常需要先创建一个客户端socket,并确保客户端socket已经正确初始化和绑定。
7.recv函数及参数返回值详解
memset(buffer, 0, sizeof(buffer));
if (recv(clientSocket, buffer, sizeof(buffer), 0) < 8)
{
close(serverSocket);
close(clientSocket);
std::cout << "recv error" << std::endl;
}
接收数据的函数,将接收的数据存放到buffer中。
8.send函数及参数返回值详解
send(clientSocket, data, sizeof(data), 0);
发送数据的函数,将要发出的数据存放到data中。
9.缓存区输出及关闭套接字
for (uint8_t value : buffer)
{
std::cout << std::hex << static_cast<int>(value) << " ";
}
std::cout << std::endl;
close(serverSocket);
close(clientSocket);
二、Socket实现TCP协议通讯源码
需要注意的是SERVER_IP可以改成本地ip,文章的源码使用的是回环地址“127.00.00.01”。
1.服务端
/*server.cpp*/
#include <iostream>
#include <string.h>
#include <cstdint>
#include <unistd.h>
#include <vector>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_PORT 8080
#define SERVER_IP "127.00.00.01"
#define LISTEN_NUM 10
#define BUFMAX 100
int main()
{
uint8_t buffer[BUFMAX];
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket < 0)
{
std::cout << "socket error" << std::endl;
return 1;
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) < 0)
{
close(serverSocket);
std::cout << "inet_pton error" << std::endl;
return -1;
}
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
{
close(serverSocket);
std::cout << "bind error" << std::endl;
return -1;
}
if (listen(serverSocket, LISTEN_NUM) < 0)
{
close(serverSocket);
std::cout << "listen error" << std::endl;
return -1;
}
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize);
if (clientSocket < 0)
{
close(serverSocket);
std::cout << "accept error" << std::endl;
return -1;
}
memset(buffer, 0, sizeof(buffer));
if (recv(clientSocket, buffer, sizeof(buffer), 0) < 8)
{
close(serverSocket);
close(clientSocket);
std::cout << "recv error" << std::endl;
}
for (uint8_t value : buffer)
{
std::cout << std::hex << static_cast<int>(value) << " ";
}
std::cout << std::endl;
close(serverSocket);
close(clientSocket);
return 0;
}
2.客户端
/*client.cpp*/
#include <iostream>
#include <string.h>
#include <cstdint>
#include <unistd.h>
#include <iomanip>
#include <vector>
#include <sys/socket.h>
#include <arpa/inet.h>
#define CLIENT_PORT 8080
#define SERVER_IP "127.00.00.01"
#define BUFMAX 50
int main()
{
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket < 0)
{
std::cout << "socket error" << std::endl;
return -1;
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(CLIENT_PORT);
if (inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr) < 0)
{
std::cout << "inet_pton error" << std::endl;
close(clientSocket);
return -1;
}
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
{
std::cout << "connect error" << std::endl;
close(clientSocket);
return -1;
}
uint8_t data[BUFMAX] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x2c, 0x17, 0x70, 0x7b,
0x0c, 0x03, 0xdc, 0x00, 0x64, 0x00, 0x03, 0x00, 0x01, 0x0d,
0x18, 0x0c, 0x4f, 0x00, 0x4c, 0x00, 0x47, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x4e, 0x05, 0x34, 0x65,
0x0c, 0x5d, 0x3a, 0x00, 0x01, 0x00, 0x1e, 0x00, 0x01, 0x02 };
send(clientSocket, data, sizeof(data), 0);
memset(data, 0, sizeof(data));
close(clientSocket);
return 0;
}
3.编译
Linux环境内使用g++编译生成可执行文件,操作如下(示例):
root@cpes:~/Desktop# g++ client.cpp -o client
root@cpes:~/Desktop# g++ server.cpp -o server
先运行server,操作如下(示例):
root@cpes:~/Desktop# ./server
再运行client,操作如下(示例):
root@cpes:~/Desktop# ./client
总结
本文介绍了如何在Linux环境下,使用c++通过Socket套接字实现TCP协议通讯。对Socket套接字中需要使用的函数、传参及函数返回值等细节问题做出了详细的解释。