前言
TCP和UDP都是工作在传输层,用于程序之间传输数据。二者之间的区别是TCP是面向连接的,而UDP是面向数据报的。那就意味着,TCP能够进行可靠的数据传输,而UDP进行不可靠的数据传输。关于TCP协议和UDP协议的详细内容可见博主的后续文章,本文的主要内容是关于TCP socket的网络编程。
接下来我们将基于TCP网络编程实现一个将小写字母转换成大写字母的网络服务器。
一、TCP Socket API
以下是关于使用TCP协议用到的socket API,这些函数都包含在头文件sys/socket.h
中。
1. socket
函数定义:
NAME //socket - create an endpoint for communication SYNOPSIS #include <sys/socket.h> int socket(int domain, int type, int protocol);
功能:
socket()
会打开一个网络通信端口,如果打开成功,则像open()
函数一样返回一个文件描述符,如果失败则返回 -1。这样网络应用程序就可以像读写文件那样使用read/write
在网络上读取和发送数据。
参数详解:
- 第一个参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。对于IPv4,domain参数指定为AF_INET,而IPv6则是AF_INET6。并且AF_INET和PFINET的值是一致的。
- 第二个参数type用于设置通信协议的族,这些族也在文件sys/socket.h中定义,包含如下表所示的值。
【补充说明】
- 类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connet()函数进行。一旦连接,可以使用read/write函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内仍然没有接收完毕,可以将这个连接认为已经断开。
- SOCK_DGRAM和SOCK_RAW这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接收数据,recvfrom()接收来自指定IP地址的发送方的数据。
- 第三个参数
protocol
用于指定某个协议的特定类型,即type类型中的某个类型。通常某个协议中只有一种特定类型,这样protocol
参数仅能设置为0,但是有些协议有多种类型,就需要设置这个参数来选择特定的类型。
2. bind
函数定义:
NAME //bind - bind a name to a socket SYNOPSIS #include <sys/socket.h> int bind(int socket, const struct sockaddr *address, socklen_t address_len);
因为服务器程序所监听的网络地址和端口号通常都是固定不变的,客户端得知了服务器程序的地址和端口号后就可以向服务器发起连接,而服务器需要绑定一个固定的网络地址和端口号。因此bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听sockaddr所描述的地址和端口号。绑定成功返回0,失败则返回-1。
博主的上一篇文章【网络套接字编程】中提到过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
在程序中myaddr
的定义及初始化如下:
struct sockaddr_in myaddr; bzero(&myaddr, sizeof(myaddr)); myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = htonl(INADDR_ANY); myaddr.sin_port = htons(SERV_PORT);
- 定义myaddr
- 使用bzero函数将整个结构体清零
- 设置网络通信的域为AF_INET
- 将网络地址设置为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址
- 最后填充端口号
虽然bind()
中的第二个参数类型是sockaddr
,但是我们真正填充信息使用的数据结构是sockaddr_in
,这个结构里主要有三部分信息:地址类型、端口号、IP地址。最后在进行函数传参的时候只需要将sockaddr_in*
强制类型转换成sockaddr
即可。
3. listen
函数定义:
NAME //listen - listen for socket connections and limit the queue of incoming connections SYNOPSIS #include <sys/socket.h> int listen(int socket, int backlog);
listen()
函数用于声明sockfd
处于监听状态,并且最多允许有backlog
个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。(详细内容可见博主的后续文章【TCP协议】)。listen()
函数调用成功返回0,调用失败则返回 -1。
4. accept
函数定义:
NAME accept - accept a new connection on a socket SYNOPSIS #include <sys/socket.h> int accept(int socket, struct sockaddr *restrict_address, socklen_t *restrict_address_len);
accept()函数的作用是,当客户端与服务端的三次握手完成后,服务器调用accept()函数接受连接。如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端请求连接。
返回值:
- 调用成功则返回客户端socket()返回的文件描述符,调用失败则返回 -1。
参数:
- 第一个参数socket即是调用socket()函数返回的文件描述符。
- 第二个参数restrict_address是输出型参数,用于获取客户端的网络地址和端口号,如果该参数为空,则表示当前服务端不关心客户端的地址。
- 第三个参数restrict_address_len也是输出型参数,它表示的是缓冲区restrict_address的长度,以避免缓冲区溢出问题,最后传出客户端地址结构体的实际长度。
accept()
函数在服务器程序中的使用结构如下:
while (true) { sockaddr_in peer_addr; socklen_t len = sizeof(peer_addr); int peer_sock = accept(_fd, (sockaddr *)&peer_addr, &len); ssize_t read_size = read(peer_sock, buf, sizeof(buf)); . . . close(peer_sock); }
5. connect
函数定义:
NAME //connect - connect a socket SYNOPSIS #include <sys/socket.h> int connect(int socket, const struct sockaddr *address, socklen_t address_len);
作用与参数说明:
connect
函数用于客户端连接服务器。其参数与bind()
函数的参数一致,区别在于bind()
函数绑定的参数是自己的地址,而connect()
函数的连接是服务器的地址。
返回值:
- 调用成功返回0,调用失败则返回 -1。
二、封装TCPSocket
#pragma once #include <iostream> #include <string> #include <cstring> #include <cassert> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define CHECK_RET(exp) \ if (!(exp)) \ { \ return false; \ } class TcpSocket { public: TcpSocket() : _fd(-1) {} ~TcpSocket() {} public: bool Socket() { _fd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET表示采用IPv4, SOCK_STREAM表示采用tcp协议 if (_fd < 0) { std::cerr << "create socket error!" << std::endl; return false; } return true; } bool Close() { close(_fd); return true; } bool Bind(const std::string &ip, uint16_t port) { sockaddr_in addr; // 填充addr信息 addr.sin_family = AF_INET; addr.sin_port = htons(port); // addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str()); ip.empty() ? (addr.sin_addr.s_addr = INADDR_ANY) : inet_aton(ip.c_str(), &addr.sin_addr); if (bind(_fd, (const sockaddr *)&addr, sizeof(addr)) < 0) { std::cerr << "bind error!" << std::endl; return false; } return true; } bool Listen(int num) { if (listen(_fd, num) < 0) { std::cerr << "listen error!" << std::endl; return false; } return true; } bool Accept(TcpSocket *peer, std::string *ip = nullptr, std::uint16_t *port = nullptr) { sockaddr_in peer_addr; socklen_t len = sizeof(peer_addr); int peer_sock = accept(_fd, (sockaddr *)&peer_addr, &len); if (peer_sock < 0) { std::cerr << "accept error!" << std::endl; return false; } peer->_fd = peer_sock; if (ip != nullptr) { *ip = inet_ntoa(peer_addr.sin_addr); } if (port != nullptr) { *port = ntohs(peer_addr.sin_port); } return true; } bool Recv(std::string *buf) { buf->clear(); char inbuf[1024 * 10] = {0}; ssize_t read_size = recv(_fd, inbuf, sizeof(inbuf), 0); if (read_size < 0) { std::cerr << "recv error!" << std::endl; return false; } if (read_size == 0) { return false; } buf->assign(inbuf, read_size); return true; } bool Send(const std::string &buf) { ssize_t write_size = send(_fd, buf.c_str(), buf.size(), 0); if (write_size < 0) { std::cerr << "send error!" << std::endl; return false; } return true; } bool Connect(const std::string &ip, uint16_t port) { sockaddr_in addr; // 填充addr信息 addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str()); if (connect(_fd, (sockaddr *)&addr, sizeof(addr)) < 0) { std::cerr << "connect error!" << std::endl; return false; } return true; } int GetFd() { return _fd; } private: int _fd; };
三、服务端的实现
1. 封装TCP通用服务器
#include "TcpSocket.hpp" #include "Task.hpp" typedef std::function<void(TcpSocket, const std::string &, uint16_t)> Handler; // void transServer(TcpSocket sock, const std::string &ip, uint16_t port) // { // std::string inbuf; // while (true) // { // if (!sock.Recv(&inbuf)) // { // // 如果读取失败,结束循环 // printf("[client %s:%d] disconnect!\n", ip.c_str(), port); // break; // } // if (strcasecmp(inbuf.c_str(), "quit") == 0) // { // printf("[client %s:%d] quit!\n", ip.c_str(), port); // break; // } // printf("transform before: %s[%d]--> %s\n", ip.c_str(), port, inbuf.c_str()); // fflush(stdout); // for (int i = 0; i < inbuf.size(); ++i) // { // if (isalpha(inbuf[i]) && islower(inbuf[i])) // { // inbuf[i] = toupper(inbuf[i]); // } // } // printf("transform after: %s[%d]--> %s\n", ip.c_str(), port, inbuf.c_str()); // fflush(stdout); // sock.Send(inbuf); // } // sock.Close(); // } class TcpServer { public: TcpServer(int port, const std::string &ip = "") : _port(port), _ip(ip) { _sock.Socket(); } ~TcpServer() {} public: bool Start(Handler handler) { // 绑定IP和端口号 CHECK_RET(_sock.Bind(_ip, _port)); // 监听 CHECK_RET(_sock.Listen(5)); // 进入循环 while (true) { // 进行accept TcpSocket peer_sock; std::string ip; uint16_t port = 0; if (!_sock.Accept(&peer_sock, &ip, &port)) { continue; } printf("[client %s:%d] connect!\n", ip.c_str(), port); // 执行任务 // TODO Task task(peer_sock, ip, port, handler); task(); } } private: // tcp socket对象 TcpSocket _sock; // 端口号 uint16_t _port; // ip地址 std::string _ip; };
2. 封装任务对象
#include <iostream> #include <string> #include <functional> #include <pthread.h> #include "TcpSocket.hpp" class Task { typedef std::function<void(TcpSocket, const std::string &, uint16_t)> callback_t; public: Task(TcpSocket sock, const std::string &ip, uint16_t port, callback_t func) :_sock(sock), _ip(ip), _port(port), _func(func) {} ~Task() {} void operator()() { printf("线程ID[%p]处理client[%s:%d]的请求开始了...\n", pthread_self(), _ip.c_str(), _port); fflush(stdout); _func(_sock, _ip, _port); printf("线程ID[%p]处理client[%s:%d]的请求结束了...\n", pthread_self(), _ip.c_str(), _port); fflush(stdout); } private: TcpSocket _sock; std::string _ip; uint16_t _port; callback_t _func; // 处理任务的回调函数 };
3. 实现转换功能的服务器
#include <iostream> #include "TcpSocket.hpp" #include "TcpServer.hpp" void transServer(TcpSocket sock, const std::string &ip, uint16_t port) { std::string inbuf; while (true) { if (!sock.Recv(&inbuf)) { // 如果读取失败,结束循环 printf("[client %s:%d] disconnect!\n", ip.c_str(), port); break; } if (strcasecmp(inbuf.c_str(), "quit") == 0) { printf("[client %s:%d] quit!\n", ip.c_str(), port); break; } printf("transform before: %s[%d]--> %s\n", ip.c_str(), port, inbuf.c_str()); fflush(stdout); for (int i = 0; i < inbuf.size(); ++i) { if (isalpha(inbuf[i]) && islower(inbuf[i])) { inbuf[i] = toupper(inbuf[i]); } } printf("transform after: %s[%d]--> %s\n", ip.c_str(), port, inbuf.c_str()); fflush(stdout); sock.Send(inbuf); } sock.Close(); } static void Usage(std::string proc) { std::cout << "Usage:\n\t" << proc << " ip port" << std::endl; std::cout << "example:\n\t" << proc << " 127.0.0.1 8080\n" << std::endl; } int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); return 1; } std::string ip; if (argc == 3) { ip = argv[1]; } uint16_t port = atoi(argv[2]); TcpServer server(port, ip); server.Start(transServer); return 0; }
四、客户端的实现
1. 封装TCP通用客户端
#include "TcpSocket.hpp" class TcpClient { public: TcpClient(const std::string& ip, uint16_t port) :_ip(ip), _port(port) { _sock.Socket(); } ~TcpClient() { _sock.Close(); } public: bool Connect() { return _sock.Connect(_ip, _port); } bool Recv(std::string* buf) { return _sock.Recv(buf); } bool Send(const std::string& buf) { _sock.Send(buf); } int GetFd() { return _sock.GetFd(); } private: TcpSocket _sock; std::string _ip; uint16_t _port; };
2. 实现转换功能的客户端
#include <iostream> #include "TcpClient.hpp" volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8080\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); return 1; } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); TcpClient client(serverIp, serverPort); // 建立连接 if (!client.Connect()) { std::cout << "connecte errer!" << std::endl; return 2; } std::cout << "connecte success! fd: " << client.GetFd() << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入您的内容# "; std::getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0) { quit = true; } if (client.Send(message)) { message.resize(message.size()); if (client.Recv(&message)) { std::cout << "Server Echo ---> " << message << std::endl; } } else { break; } } return 0; }
五、结果演示
启动服务端:
启动客户端:
客户端连接服务端成功,此时服务端状态:
测试样例:
客户端输入样例,发现转换成功。
存在的问题:
此时,启动又一个客户端连接服务器,在此输入时,我们会发现输入的内容会卡住显示屏上。
此时我们关闭第一个客户端后发现有得出转换后的结果。
原因在于当前的服务器是单进程版本的,只能够同时为一个客户端服务,所以再来一个客户端会阻塞等待服务器结束对上一个客户端的服务。以下的改进的多进程和多线程版本的服务器。
六、多进程版服务器
改进思想是,父进程为每个客户端的请求都创建一个子进程去处理任务,父进程不做任何工作,但要注意的是父进程中要关闭不断创建的客户端的peer_sock,避免内存泄漏。子进程执行客户端的请求,在请求结束后调用exit函数退出,但不必单独释放_sock对象,因为会自动调用其析构函数。同时设置signal函数,对SIGCHLD做忽略动作,使得父进程不必等待子进程,只处理自己的任务。
#pragma once #include "TcpSocket.hpp" #include "Task.hpp" #include <cassert> #include <signal.h> #include <unistd.h> typedef std::function<void(TcpSocket, const std::string &, uint16_t)> Handler; class TcpProcessServer { public: TcpProcessServer(int port, const std::string &ip = "") : _port(port), _ip(ip) { _sock.Socket(); signal(SIGCHLD, SIG_IGN); } ~TcpProcessServer() {} public: bool Start(Handler handler) { // 绑定IP和端口号 CHECK_RET(_sock.Bind(_ip, _port)); // 监听 CHECK_RET(_sock.Listen(5)); // 进入循环 while (true) { // 进行accept TcpSocket peer_sock; std::string ip; uint16_t port = 0; if (!_sock.Accept(&peer_sock, &ip, &port)) { continue; } printf("[client %s:%d] connect!\n", ip.c_str(), port); // 执行任务 // 多进程 v1.0 pid_t id = fork(); if (id > 0) { // 父进程,不需要做什么 peer_sock.Close(); // 父进程中需要关闭 } else if (id == 0) { // 子进程 // 子进程的 socket 的关闭在析构函数中进行 Task task(peer_sock, ip, port, handler); task(); // 处理任务结束,退出子进程 exit(0); } else { // fork失败 std::cerr << "fork error!" << std::endl; return false; } } return true; } private: // tcp socket对象 TcpSocket _sock; // 端口号 uint16_t _port; // ip地址 std::string _ip; };
结果:
此时服务器便可以为多个客户端同时进行服务。
七、线程池版服务器
实现线程池:
#pragma once #include <iostream> #include <queue> #include <cassert> #include <pthread.h> const uint32_t gDefaultThreadNum = 5; // 默认线程池中线程数量 template <class T> class ThreadPool { public: ThreadPool(uint32_t threadNum = gDefaultThreadNum) : _isStart(false), _ThreadNum(threadNum) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } public: // 启动线程池 void start() { // 判断线程池是否启动 assert(!_isStart); // 如果已经启动则失败 for (int i = 0; i < _ThreadNum; ++i) { pthread_t tid; pthread_create(&tid, nullptr, threadRoutine, this); } // 线程池已经启动 _isStart = true; } // 放入任务 void push(const T &in) { lockQueue(); _taskQueue.push(in); handleTask(); unlockQueue(); } // 消费任务 T pop() { T task = _taskQueue.front(); _taskQueue.pop(); return task; } private: static void *threadRoutine(void *args) { ThreadPool<T> *ptp = static_cast<ThreadPool<T> *>(args); while (true) { ptp->lockQueue(); // 判断当前任务队列中有没有任务 while (!ptp->hasTask()) { // 没有任务,进行循环等待 ptp->waitTask(); } // 当前线程获取任务 T t = ptp->pop(); ptp->unlockQueue(); // 当前线程处理任务 t(); } } void lockQueue() { pthread_mutex_lock(&_mutex); } void unlockQueue() { pthread_mutex_unlock(&_mutex); } void waitTask() { pthread_cond_wait(&_cond, &_mutex); } void handleTask() { pthread_cond_signal(&_cond); } bool hasTask() { return !_taskQueue.empty(); } private: bool _isStart; // 判断线程池是否开启 uint32_t _ThreadNum; // 线程池中的线程数量 std::queue<T> _taskQueue; // 任务队列 pthread_mutex_t _mutex; // 保护任务队列的锁 pthread_cond_t _cond; // 线程池的条件变量 };
线程池版服务器:
#pragma once #include "TcpSocket.hpp" #include "Task.hpp" #include "ThreadPool.hpp" typedef std::function<void(TcpSocket, const std::string &, uint16_t)> Handler; class TcpThreadPoolServer { public: TcpThreadPoolServer(int port, const std::string &ip = "") : _port(port), _ip(ip) { _sock.Socket(); _tp.start(); } ~TcpThreadPoolServer() {} public: bool Start(Handler handler) { // 绑定IP和端口号 CHECK_RET(_sock.Bind(_ip, _port)); // 监听 CHECK_RET(_sock.Listen(5)); // 进入循环 while (true) { // 进行accept TcpSocket peer_sock; std::string ip; uint16_t port = 0; if (!_sock.Accept(&peer_sock, &ip, &port)) { continue; } printf("[client %s:%d] connect!\n", ip.c_str(), port); // 执行任务 // TODO Task task(peer_sock, ip, port, handler); _tp.push(task); } } private: // tcp socket对象 TcpSocket _sock; // 端口号 uint16_t _port; // ip地址 std::string _ip; // 线程池 ThreadPool<Task> _tp; };
结果