目录
一、客户端套接字创建
1、客户端套接字创建概述
-
在TCP网络通信中,客户端的实现同样需要创建套接字作为通信的基础。与服务器端不同,客户端的套接字创建过程更为简单,不需要进行绑定(bind)和监听(listen)操作。
-
同样地,我们将客户端封装为一个类。在实例化客户端对象后,需要进行初始化操作,其核心任务就是创建套接字。值得注意的是,客户端调用socket函数创建套接字时,其参数设置与服务端完全一致。
下面我们将详细介绍客户端套接字的创建过程,并展示一个完整的TCP客户端类实现。
2、客户端与服务端套接字创建的区别(重点!!!)
共同点
客户端和服务端在创建套接字时都使用相同的socket()系统调用,参数设置也完全一致:
-
地址族(AF_INET):表示使用IPv4协议
-
套接字类型(SOCK_STREAM):表示面向连接的TCP协议
-
协议(0):表示使用默认协议(对于SOCK_STREAM就是TCP)
不同点
绑定操作:
-
服务端:必须显式绑定到特定的IP地址和端口号,因为这些信息需要被客户端知晓以便连接
-
客户端:不需要显式绑定,系统会自动分配一个临时端口号给客户端套接字
监听操作:
-
服务端:需要调用
listen()函数开始监听连接请求 -
客户端:不需要监听,因为不会有其他客户端主动连接它
连接目标:
-
服务端:被动等待客户端连接
-
客户端:必须明确知道要连接的服务端IP地址和端口号
3、TCP客户端类实现的套接字创建
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class TcpClient
{
public:
TcpClient(std::string server_ip, int server_port)
: _sock(-1)
, _server_ip(server_ip)
, _server_port(server_port)
{}
void InitClient()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpClient()
{
if (_sock >= 0){
close(_sock);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
二、客户端连接服务器
1、客户端连接服务器的基本流程
在TCP网络编程中,客户端连接服务器的过程相对服务器端来说更为简单。客户端不需要进行绑定(bind)操作,也不需要监听(listen)连接请求。客户端的主要任务是创建套接字后,直接向服务器发起连接请求。
为什么客户端不需要显式绑定?
虽然技术上客户端不需要我们手动进行绑定操作,但实际上系统会在客户端发起连接请求时自动为其分配一个临时端口号。这是因为:
-
通信双方都必须有明确的IP地址和端口号才能建立连接
-
客户端也需要一个端口号来标识自己,以便服务器知道将响应发送到哪里
-
系统会自动为客户端选择一个临时端口(ephemeral port),通常范围在32768-60999之间
2、connect函数
客户端需要通过调用 connect() 函数来建立与服务器的连接。
connect() 和 bind() 的参数格式相同,区别在于:
-
bind()绑定的是本地地址 -
connect()指定的是服务器地址
客户端发起连接请求的核心函数是connect(),其函数原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明
-
sockfd:已经创建好的套接字描述符,通过该套接字发起连接请求
-
addr:指向包含对端(服务器)网络信息的结构体指针,通常使用
sockaddr_in结构体并强制转换 -
addrlen:传入的结构体长度,通常使用
sizeof()计算
返回值
-
成功时返回0
-
失败时返回-1,并设置errno错误码

3、TCP客户端类实现的连接服务器
此外,在调用connect函数发起连接请求时,必须传入服务端的网络信息参数。缺少这些参数,connect函数将无法确定客户端需要连接的具体服务端目标。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class TcpClient
{
public:
TcpClient(std::string server_ip, int server_port)
: _sock(-1)
, _server_ip(server_ip)
, _server_port(server_port)
{}
void InitClient()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpClient()
{
if (_sock >= 0){
close(_sock);
}
}
void Start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
std::cout << "connect success..." << std::endl;
Request(); //发起请求
}
else{ //connect error
std::cerr << "connect failed..." << std::endl;
exit(3);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
三、客户端发起请求
我们实现的是一个简单的回声服务器。当客户端连接成功后,可以直接向服务端发送数据。具体实现方式是:客户端将用户输入的数据通过write函数写入套接字。
服务端收到数据后会进行回显,因此客户端在发送数据后,还需要调用read函数读取服务端的响应数据。最后将接收到的响应数据打印出来,以验证双方通信是否正常。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class TcpClient
{
public:
TcpClient(std::string server_ip, int server_port)
: _sock(-1)
, _server_ip(server_ip)
, _server_port(server_port)
{}
void InitClient()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
~TcpClient()
{
if (_sock >= 0){
close(_sock);
}
}
void Request()
{
std::string msg;
char buffer[1024];
while (true){
std::cout << "Please Enter# ";
getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
if (size > 0){
buffer[size] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
else if (size == 0){
std::cout << "server close!" << std::endl;
break;
}
else{
std::cerr << "read error!" << std::endl;
break;
}
}
}
void Start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
std::cout << "connect success..." << std::endl;
Request(); //发起请求
}
else{ //connect error
std::cerr << "connect failed..." << std::endl;
exit(3);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
运行客户端程序时,需携带服务端的IP地址和端口号。通过这些信息构建客户端对象,初始化后即可启动客户端。
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3){
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
TcpClient* clt = new TcpClient(server_ip, server_port);
clt->InitClient();
clt->Start();
return 0;
}
四、服务器测试
服务器端与客户端程序开发已完成,接下来进入测试阶段。
测试步骤如下:首先启动服务端程序,随后执行netstat命令进行状态检查。此时可以观察到名为TcpServer的服务进程处于LISTEN监听状态。
netstat -nltp

运行客户端时,使用命令./TcpClient IP地址 端口号,客户端将自动向指定服务端发起连接请求。服务端接收到请求后,会立即为该客户端建立连接并提供相应服务。

当客户端向服务端发送消息时,服务端可通过解析消息中的IP地址和端口号来识别客户端身份(也就是说服务端监听(listen)到客户端的请求后,再连接(accept)之后,会自动将客户端的信息存到accept函数的第二个参数结构体中,系统完成这一步自动处理,不需要我们手动完成)。同时,客户端也能通过接收服务端的响应消息确认其请求是否被成功接收。

当客户端断开连接时,服务端调用read函数会返回0。这个返回值表明连接已断开,服务端随即终止对该客户端的服务。

请注意:此时是服务端终止了对该客户端的服务,而非服务器本身停止运行。服务器仍在运行中,并持续等待接收下一个客户端的连接请求。
TCP回声客户端实现详解
585

被折叠的 条评论
为什么被折叠?



