二、Socket 编程 TCP
本文介绍了TCP Socket编程的基本流程和服务器演进版本。主要内容包括:1. TCP编程基础:服务端流程(socket-bind-listen-accept-read/write-close)和客户端流程(socket-connect-read/write-close),重点说明监听socket和通信socket的区别。2. 服务器演进版本:从单进程到多进程、多线程,最终到线程池版本,逐步提高
Socket 编程 TCP
一、TCP 编程整体认识
TCP 是面向连接的可靠传输协议。和 UDP 不同,UDP 可以直接 sendto/recvfrom 收发数据,而 TCP 通信之前必须先建立连接。
TCP 服务端基本流程:
socket() -> bind() -> listen() -> accept() -> read/write -> close()
TCP 客户端基本流程:
socket() -> connect() -> write/read -> close()
服务端中有两个非常重要的 socket:
_listenSock:监听 socket,只负责获取新连接; sockfd:通信 socket,由 accept 返回,负责和某个客户端通信。
accept() 是 TCP 服务端非常关键的接口。监听 socket 并不直接和客户端通信,它只是负责等待连接。真正与客户端通信的是 accept() 返回的新 socket。
客户端一般不需要显式 bind(),因为客户端不需要固定端口。当客户端调用 connect() 时,操作系统会自动为客户端分配本地 IP 和临时端口。
二、TCP 常用接口说明
1. socket
int socket(int domain, int type, int protocol);
TCP 使用:
socket(AF_INET, SOCK_STREAM, 0);
含义:
AF_INET:IPv4; SOCK_STREAM:面向字节流,也就是 TCP; 0:使用默认协议。
2. bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
服务器需要绑定固定 IP 和端口。否则客户端不知道连接哪里。
常见写法:
local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY 表示绑定本机任意 IP。
3. listen
int listen(int sockfd, int backlog);
listen() 把 socket 设置为监听状态。只有调用 listen() 后,服务器才可以接收客户端连接。
4. accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept() 从监听队列中获取一个已经建立好的连接,并返回新的通信 socket。
5. connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端通过 connect() 连接服务器。connect() 填的是服务器地址,不是自己的地址。
6. read/write
TCP 连接建立后,可以像读写文件一样读写 socket:
read(sockfd, buffer, sizeof(buffer)); write(sockfd, data.c_str(), data.size());
但是 TCP 是面向字节流的,不保证一次 write() 对应一次完整 read()。这就是后面要解决的粘包问题。
三、服务器版本演进
V1:单进程版本
主进程 accept() 一个连接后,直接调用 Service() 处理客户端。
优点是简单,适合理解 TCP 基本流程。
缺点是一次只能服务一个客户端。如果当前客户端不退出,服务器就无法继续处理其他客户端。
V2:多进程版本
父进程只负责 accept(),每来一个客户端,就创建子进程处理。
优点是多个客户端可以并发处理。
缺点是进程创建成本较高,并且需要处理子进程回收问题,否则会产生僵尸进程。
V3:多线程版本
主线程只负责 accept(),每来一个客户端,就创建一个线程处理。
优点是比多进程更轻量。
缺点是客户端太多时,线程数量会快速增加,服务器压力变大。
V4:线程池版本
提前创建固定数量的工作线程,主线程负责接收连接,然后把任务投递到线程池。
优点是避免频繁创建和销毁线程,也能控制并发数量。
缺点是如果每个连接都是长连接,一个连接会长期占用一个线程。更高并发场景还需要继续学习 select/poll/epoll。
公共代码
下面这些文件四个版本都可以共用。
Comm.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
enum
{
Usage_Err = 1,
Socket_Err,
Bind_Err,
Listen_Err,
Connect_Err
};
// 把 sockaddr_in* 转成 sockaddr*
#define CONV(addr_ptr) ((struct sockaddr *)(addr_ptr))
nocopy.hpp
#pragma once
class nocopy
{
public:
nocopy() = default;
~nocopy() = default;
nocopy(const nocopy &) = delete;
const nocopy &operator=(const nocopy &) = delete;
};
Log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
enum LogLevel
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
class Log
{
public:
void LogMessage(LogLevel level, const char *format, ...)
{
const char *levelString[] = {
"Debug",
"Info",
"Warning",
"Error",
"Fatal"};
char timeBuffer[64];
time_t curr = time(nullptr);
struct tm *tm = localtime(&curr);
snprintf(timeBuffer, sizeof(timeBuffer),
"%04d-%02d-%02d %02d:%02d:%02d",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec);
printf("[%s][%s] ", timeBuffer, levelString[level]);
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
};
static Log lg;
InetAddr.hpp
#pragma once
#include <string>
#include <cstdint>
#include <netinet/in.h>
class InetAddr
{
public:
InetAddr();
explicit InetAddr(const struct sockaddr_in &addr);
std::string Ip() const;
uint16_t Port() const;
std::string ToString() const;
const struct sockaddr_in &Addr() const;
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
InetAddr.cc
#include "InetAddr.hpp"
#include <cstring>
#include <arpa/inet.h>
InetAddr::InetAddr()
: _ip("0.0.0.0"), _port(0)
{
memset(&_addr, 0, sizeof(_addr));
}
InetAddr::InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
char ipBuffer[64];
// inet_ntop 是线程安全版本,比 inet_ntoa 更推荐
inet_ntop(AF_INET, &_addr.sin_addr, ipBuffer, sizeof(ipBuffer));
_ip = ipBuffer;
_port = ntohs(_addr.sin_port);
}
std::string InetAddr::Ip() const
{
return _ip;
}
uint16_t InetAddr::Port() const
{
return _port;
}
std::string InetAddr::ToString() const
{
return _ip + ":" + std::to_string(_port);
}
const struct sockaddr_in &InetAddr::Addr() const
{
return _addr;
}
V1 单进程 Echo Server
TcpServer.hpp
#pragma once
#include <cstdint>
#include "nocopy.hpp"
#include "InetAddr.hpp"
const static int defaultBacklog = 6;
class TcpServer : public nocopy
{
public:
explicit TcpServer(uint16_t port);
~TcpServer();
void Init();
void Start();
private:
void Service(int sockfd, InetAddr peer);
private:
uint16_t _port;
int _listenSock;
bool _isRunning;
};
TcpServer.cc
#include "TcpServer.hpp"
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Log.hpp"
TcpServer::TcpServer(uint16_t port)
: _port(port), _listenSock(-1), _isRunning(false)
{
}
void TcpServer::Init()
{
// 防止向已经关闭的连接写入时,进程被 SIGPIPE 杀掉
signal(SIGPIPE, SIG_IGN);
// 1. 创建监听 socket
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
exit(Socket_Err);
}
// 2. 设置端口复用,方便服务器重启
int opt = 1;
setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 填写服务器本地地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
// 4. 绑定 IP 和端口
if (bind(_listenSock, CONV(&local), sizeof(local)) < 0)
{
lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
exit(Bind_Err);
}
// 5. 设置监听状态
if (listen(_listenSock, defaultBacklog) < 0)
{
lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
exit(Listen_Err);
}
lg.LogMessage(Info, "server init success, port: %d\n", _port);
}
void TcpServer::Start()
{
_isRunning = true;
while (_isRunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept 返回的是通信 socket
int sockfd = accept(_listenSock, CONV(&peer), &len);
if (sockfd < 0)
{
lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
continue;
}
InetAddr addr(peer);
lg.LogMessage(Info, "new connection: %s, sockfd: %d\n",
addr.ToString().c_str(), sockfd);
// V1:直接在主进程中提供服务
Service(sockfd, addr);
close(sockfd);
}
}
void TcpServer::Service(int sockfd, InetAddr peer)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client[" << peer.ToString() << "] say# "
<< buffer << std::endl;
std::string echo = "server echo# ";
echo += buffer;
write(sockfd, echo.c_str(), echo.size());
}
else if (n == 0)
{
lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
break;
}
else
{
lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
break;
}
}
}
TcpServer::~TcpServer()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
V2 多进程 Echo Server
V2 的核心变化:accept() 新连接后,创建子进程处理客户端。
TcpServer.hpp
#pragma once
#include <cstdint>
#include "nocopy.hpp"
#include "InetAddr.hpp"
const static int defaultBacklog = 6;
class TcpServer : public nocopy
{
public:
explicit TcpServer(uint16_t port);
~TcpServer();
void Init();
void Start();
private:
void Service(int sockfd, InetAddr peer);
void ProcessConnection(int sockfd, const struct sockaddr_in &peer);
private:
uint16_t _port;
int _listenSock;
bool _isRunning;
};
TcpServer.cc
#include "TcpServer.hpp"
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Log.hpp"
TcpServer::TcpServer(uint16_t port)
: _port(port), _listenSock(-1), _isRunning(false)
{
}
void TcpServer::Init()
{
signal(SIGPIPE, SIG_IGN);
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
exit(Socket_Err);
}
int opt = 1;
setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(_listenSock, CONV(&local), sizeof(local)) < 0)
{
lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
exit(Bind_Err);
}
if (listen(_listenSock, defaultBacklog) < 0)
{
lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
exit(Listen_Err);
}
lg.LogMessage(Info, "server init success, port: %d\n", _port);
}
void TcpServer::Start()
{
_isRunning = true;
while (_isRunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listenSock, CONV(&peer), &len);
if (sockfd < 0)
{
lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
continue;
}
ProcessConnection(sockfd, peer);
}
}
void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
InetAddr addr(peer);
pid_t id = fork();
if (id < 0)
{
lg.LogMessage(Error, "fork error, errno: %d, %s\n", errno, strerror(errno));
close(sockfd);
return;
}
else if (id == 0)
{
// 子进程不需要监听 socket
close(_listenSock);
Service(sockfd, addr);
close(sockfd);
exit(0);
}
else
{
// 父进程不负责通信,关闭自己的通信 socket
close(sockfd);
// 非阻塞回收,避免僵尸进程堆积
while (waitpid(-1, nullptr, WNOHANG) > 0)
{
}
}
}
void TcpServer::Service(int sockfd, InetAddr peer)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client[" << peer.ToString() << "] say# "
<< buffer << std::endl;
std::string echo = "server echo# ";
echo += buffer;
write(sockfd, echo.c_str(), echo.size());
}
else if (n == 0)
{
lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
break;
}
else
{
lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
break;
}
}
}
TcpServer::~TcpServer()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
V3 多线程 Echo Server
V3 的核心变化:每个客户端连接交给一个线程处理。
TcpServer.hpp
#pragma once
#include <cstdint>
#include <pthread.h>
#include "nocopy.hpp"
#include "InetAddr.hpp"
const static int defaultBacklog = 6;
class TcpServer : public nocopy
{
public:
explicit TcpServer(uint16_t port);
~TcpServer();
void Init();
void Start();
private:
class ThreadData
{
public:
ThreadData(TcpServer *server, int sockfd, const struct sockaddr_in &peer);
public:
TcpServer *_server;
int _sockfd;
InetAddr _peer;
};
private:
static void *ThreadRoutine(void *args);
void Service(int sockfd, InetAddr peer);
void ProcessConnection(int sockfd, const struct sockaddr_in &peer);
private:
uint16_t _port;
int _listenSock;
bool _isRunning;
};
TcpServer.cc
#include "TcpServer.hpp"
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Log.hpp"
TcpServer::ThreadData::ThreadData(TcpServer *server, int sockfd, const struct sockaddr_in &peer)
: _server(server), _sockfd(sockfd), _peer(peer)
{
}
TcpServer::TcpServer(uint16_t port)
: _port(port), _listenSock(-1), _isRunning(false)
{
}
void TcpServer::Init()
{
signal(SIGPIPE, SIG_IGN);
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
exit(Socket_Err);
}
int opt = 1;
setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(_listenSock, CONV(&local), sizeof(local)) < 0)
{
lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
exit(Bind_Err);
}
if (listen(_listenSock, defaultBacklog) < 0)
{
lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
exit(Listen_Err);
}
lg.LogMessage(Info, "server init success, port: %d\n", _port);
}
void TcpServer::Start()
{
_isRunning = true;
while (_isRunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listenSock, CONV(&peer), &len);
if (sockfd < 0)
{
lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
continue;
}
ProcessConnection(sockfd, peer);
}
}
void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
pthread_t tid;
ThreadData *td = new ThreadData(this, sockfd, peer);
int n = pthread_create(&tid, nullptr, ThreadRoutine, td);
if (n != 0)
{
lg.LogMessage(Error, "pthread_create error\n");
close(sockfd);
delete td;
}
}
void *TcpServer::ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_server->Service(td->_sockfd, td->_peer);
close(td->_sockfd);
delete td;
return nullptr;
}
void TcpServer::Service(int sockfd, InetAddr peer)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client[" << peer.ToString() << "] say# "
<< buffer << std::endl;
std::string echo = "server echo# ";
echo += buffer;
write(sockfd, echo.c_str(), echo.size());
}
else if (n == 0)
{
lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
break;
}
else
{
lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
break;
}
}
}
TcpServer::~TcpServer()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
V4 线程池 Echo Server
ThreadPool.hpp
#pragma once
#include <queue>
#include <mutex>
#include <thread>
#include <vector>
#include <condition_variable>
template <class Task>
class ThreadPool
{
public:
static ThreadPool<Task> *GetInstance()
{
static ThreadPool<Task> instance;
return &instance;
}
void Start()
{
std::lock_guard<std::mutex> lock(_startMutex);
if (_isRunning)
{
return;
}
_isRunning = true;
for (int i = 0; i < _threadNum; ++i)
{
_threads.emplace_back([this]() {
this->ThreadRun();
});
}
for (auto &thread : _threads)
{
thread.detach();
}
}
void Push(const Task &task)
{
{
std::lock_guard<std::mutex> lock(_mutex);
_tasks.push(task);
}
_cond.notify_one();
}
private:
ThreadPool(int threadNum = 5)
: _threadNum(threadNum), _isRunning(false)
{
}
ThreadPool(const ThreadPool &) = delete;
ThreadPool &operator=(const ThreadPool &) = delete;
void ThreadRun()
{
while (true)
{
Task task;
{
std::unique_lock<std::mutex> lock(_mutex);
_cond.wait(lock, [this]() {
return !_tasks.empty();
});
task = _tasks.front();
_tasks.pop();
}
task();
}
}
private:
int _threadNum;
bool _isRunning;
std::vector<std::thread> _threads;
std::queue<Task> _tasks;
std::mutex _mutex;
std::mutex _startMutex;
std::condition_variable _cond;
};
TcpServer.hpp
#pragma once
#include <cstdint>
#include "nocopy.hpp"
#include "InetAddr.hpp"
const static int defaultBacklog = 6;
class TcpServer : public nocopy
{
public:
explicit TcpServer(uint16_t port);
~TcpServer();
void Init();
void Start();
private:
void Service(int sockfd, InetAddr peer);
void ProcessConnection(int sockfd, const struct sockaddr_in &peer);
private:
uint16_t _port;
int _listenSock;
bool _isRunning;
};
TcpServer.cc
#include "TcpServer.hpp"
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
TcpServer::TcpServer(uint16_t port)
: _port(port), _listenSock(-1), _isRunning(false)
{
}
void TcpServer::Init()
{
signal(SIGPIPE, SIG_IGN);
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
lg.LogMessage(Fatal, "socket error, errno: %d, %s\n", errno, strerror(errno));
exit(Socket_Err);
}
int opt = 1;
setsockopt(_listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(_listenSock, CONV(&local), sizeof(local)) < 0)
{
lg.LogMessage(Fatal, "bind error, errno: %d, %s\n", errno, strerror(errno));
exit(Bind_Err);
}
if (listen(_listenSock, defaultBacklog) < 0)
{
lg.LogMessage(Fatal, "listen error, errno: %d, %s\n", errno, strerror(errno));
exit(Listen_Err);
}
using task_t = std::function<void()>;
ThreadPool<task_t>::GetInstance()->Start();
lg.LogMessage(Info, "server init success, port: %d\n", _port);
}
void TcpServer::Start()
{
_isRunning = true;
while (_isRunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listenSock, CONV(&peer), &len);
if (sockfd < 0)
{
lg.LogMessage(Warning, "accept error, errno: %d, %s\n", errno, strerror(errno));
continue;
}
ProcessConnection(sockfd, peer);
}
}
void TcpServer::ProcessConnection(int sockfd, const struct sockaddr_in &peer)
{
using task_t = std::function<void()>;
InetAddr addr(peer);
// 把连接封装成任务,交给线程池处理
task_t task = std::bind(&TcpServer::Service, this, sockfd, addr);
ThreadPool<task_t>::GetInstance()->Push(task);
}
void TcpServer::Service(int sockfd, InetAddr peer)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client[" << peer.ToString() << "] say# "
<< buffer << std::endl;
std::string echo = "server echo# ";
echo += buffer;
write(sockfd, echo.c_str(), echo.size());
}
else if (n == 0)
{
lg.LogMessage(Info, "client quit: %s\n", peer.ToString().c_str());
break;
}
else
{
lg.LogMessage(Error, "read error, errno: %d, %s\n", errno, strerror(errno));
break;
}
}
close(sockfd);
}
TcpServer::~TcpServer()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
客户端源码
四个版本都可以使用这个客户端测试。
TcpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
static void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return Usage_Err;
}
std::string serverIp = argv[1];
uint16_t serverPort = std::stoi(argv[2]);
// 1. 创建 socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return Socket_Err;
}
// 2. 填写服务器地址
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
if (inet_pton(AF_INET, serverIp.c_str(), &server.sin_addr) <= 0)
{
std::cerr << "inet_pton error" << std::endl;
close(sockfd);
return Connect_Err;
}
// 3. 连接服务器
// 客户端通常不需要显式 bind,connect 时系统会自动绑定本地端口
if (connect(sockfd, CONV(&server), sizeof(server)) < 0)
{
std::cerr << "connect error" << std::endl;
close(sockfd);
return Connect_Err;
}
std::cout << "connect server success" << std::endl;
// 4. 通信
while (true)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line);
if (line == "quit" || line == "exit")
{
break;
}
ssize_t n = write(sockfd, line.c_str(), line.size());
if (n <= 0)
{
std::cerr << "write error" << std::endl;
break;
}
char buffer[1024];
ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
if (m > 0)
{
buffer[m] = '\0';
std::cout << buffer << std::endl;
}
else if (m == 0)
{
std::cout << "server close connection" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
close(sockfd);
return 0;
}
服务端启动入口
ServerMain.cc
#include <iostream>
#include <cstdlib>
#include "TcpServer.hpp"
#include "Comm.hpp"
static void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
TcpServer server(port);
server.Init();
server.Start();
return 0;
}
Makefile
.PHONY: all clean
CXX=g++
CXXFLAGS=-std=c++17 -Wall -Wextra -pthread
all: tcp_server tcp_client
tcp_server: ServerMain.cc TcpServer.cc InetAddr.cc
$(CXX) $(CXXFLAGS) -o $@ $^
tcp_client: TcpClient.cc
$(CXX) $(CXXFLAGS) -o $@ $^
clean:
rm -f tcp_server tcp_client
运行测试
编译:
make
启动服务端:
./tcp_server 8080
启动客户端:
./tcp_client 127.0.0.1 8080
客户端输入:
hello
服务器返回:
server echo# hello
输入:
quit
客户端退出。
最后总结
TCP 编程的主线是连接。
服务端先创建监听 socket,然后绑定端口,接着调用 listen() 进入监听状态。之后通过 accept() 获取客户端连接。accept() 返回的新 socket 才是真正用于通信的 socket。
单进程版本适合理解流程;多进程版本可以支持并发;多线程版本比多进程更轻量;线程池版本进一步减少线程频繁创建销毁的开销。
不过这些版本目前都还是简单 Echo Server,还没有解决 TCP 粘包问题。因为 TCP 是字节流协议,不保留消息边界。真正写业务服务器时,还需要设计应用层协议,例如:
报文长度 + 报文内容
后面继续学习协议设计、序列化反序列化、线程池任务处理、IO 多路复用时,TCP 服务器的结构就会越来越完整
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)