C/C++ Linux网络编程3 - Socket编程与TCP服务器客户端
上篇文章:C/C++ Linux网络编程2 - Socket编程与简单UDP服务器客户端-CSDN博客
代码仓库:橘子真甜 (yzc-YZC) - Gitee.com
目录
一. TCP服务器API
1.1 监听函数listen
1.2 接收函数accept
1.3 连接远端connect
1.4 数据接收recv
编辑
1.5 数据发送send
二. TcpServer
2.1 tcpServer.hpp
2.2 tcpServer.cc
三. TcpClient
3.1 tcpClient.hpp
3.2 tcpClient.cc
四. 代码测试
一. TCP服务器API
udp的特点是:不保证可靠传输,面向数据报,无连接。
tcp的特点是:可靠传输,面向字节流,有连接
因此,在UDP服务器中,没有特殊要求的话。一般只要socket,bind之后就能传输数据了。并且需要一次性接收/发送一个完整的报文
而TCP是有连接的,所以我们的tcp服务器需要管理好每一个连接。既然有连接,还要监听和接收网络中的连接。这需要使用listten (监听) 和 accept (接收)
同时面向字节流,读取数据也有自己的要求(tcp服务器接收的数据是保证有序的)。但是tcp发送的数据会有粘包问题,需要根据应用的需要进行分包
下面详细介绍接口 listen 和 accept
1.1 监听函数listen
首先看看手册中对listen的介绍

总结下来就是这样
//所需头文件
#include <sys/types.h>
#include <sys/socket.h>//函数原型,用于将一个sockfd设置为监听fd,并且建立好半连接/全连接队列用于三次握手
int listen(int sockfd, int backlog);//参数说明
sock : 用于监听网络连接的文件fd
backlog : 当前版本的作用是设置 全连接队列的大小//返回值
成功返回0,失败返回 -1
注意:
listen之后tcp服务器会创建syn_queue(半连接队列) 和 accept_queue(全连接队列),之后才能进行三次握手。
需要根据服务器的需要提供合适的backlog,一般建议设置为128
1.2 接收函数accept
首先看手册中的内容

#include <sys/types.h>
#include <sys/socket.h>//用于获取来自客户端的新连接,其中addr中包含了来自客户端的信息 ip,端口等
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//参数说明
sockfd : listen设置好的监听文件fd
addr : 用于获取对面的ip/port/通信协议等信息
addrlen : 和addr匹配的数据//成功返回一个文件描述符,失败返回-1
accept用于获取新的网络连接,并返回一个文件fd用于连接双方的通信
1.3 连接远端connect
用于连接指定ip/port的服务器,并且发起三次握手

#include <sys/types.h>
#include <sys/socket.h>//函数原型
//用于向服务端发起连接请求,内部会自动帮助我们绑定自己ip 端口与套接字
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);//connect接口与bind非常类型
//不过bind绑定的addr内部是自己的ip和端口,而connect是连接服务端的ip和端口//connet成功返回0,失败返回-1
1.4 数据接收recv
该函数用于接收数据,与read非常像
//用于接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);//参数说明
sockfd 用于接收数据的fd
buf 用于接收数据的用户缓冲区
len 表示接收数据的长度
flags 接收数据的方式,一般填0表示默认,即阻塞接收返回值
返回接收到数据的大小,小于0表示接收错误
1.5 数据发送send

//用于接收数据
ssize_t send(int socket, const void *buffer, size_t length, int flags);//参数说明
sockfd 用于接收数据的fd
buffer 用于发送数据的用户缓冲区
length 表示接收数据的长度
flags 发送数据的方式,一般填0表示默认。阻塞发送返回值
返回接发送数据的大小,小于0表示发送错误
二. TcpServer
2.1 tcpServer.hpp
与UDP一样,我们先构建代码的整体框架,我们通过设置好的回调函数来处理网络IO
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>#include <functional>namespace YZC
{// 设置默认端口和最大backlogconst int defaultPort = 8080;const int maxBacklog = 128;// 设置回调函数using func_t = std::function<void(int)>;//typedef void (*func_t)(int);class tcpServer{public:tcpServer(func_t func, int port = defaultPort): _port(port), _callback(func) {}private:int _listensock;int _port;func_t _callback;};}
初始化init函数
注意点:
1 tcp是面向字节流的,所以socket第二个参数需要设置为 SOCK_STREAM
void init(){// 1.创建socket_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "sockte err" << std::endl;exit(-1);}// 2 bind绑定fd和端口struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));// 设置地址的信息(协议,ip,端口)serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址serveraddr.sin_port = htons(_port); // 注意端口16位,2字节需要使用htons。不可socklen_t len = sizeof(serveraddr);if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0){std::cerr << "bind err" << std::endl;exit(-1);}// 3 设置sockfd为监听fdif (listen(_listensock, maxBacklog) < 0){std::cerr << "listen err" << std::endl;exit(-1);}}
至此,tcpserver的初始化就结束了。过程是 socket bind listen,之后就能通过accept获取来自网络中的连接用于通信了
服务器run函数
void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}// 到这里就能通过sockfd进行通信了// 通过clientaddr获取对方的ip/portstd::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);}}
serviceIO函数
server的服务,用于处理数据,回应客户端
void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}printf("client --> server:%s\n", buffer);// 直接将数据返回给clientcount = send(sockfd, buffer, strlen(buffer), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}}
全部代码如下:增加了少量的打印信息
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>#include <functional>namespace YZC
{// 设置默认端口和最大backlogconst int defaultPort = 8080;const int maxBacklog = 128;// 设置回调函数using func_t = std::function<void(int)>;// typedef void (*func_t)(int);class tcpServer{public:tcpServer(func_t func, int port = defaultPort): _port(port), _callback(func) {}void init(){// 1.创建socket_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "sockte err" << std::endl;exit(-1);}std::cout << "socket success" << std::endl;// 2 bind绑定fd和端口struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(serveraddr));// 设置地址的信息(协议,ip,端口)serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定任意网卡ip,通常我们访问某一个IP地址是这个服务器的公网网卡IP地址serveraddr.sin_port = htons(_port); // 注意端口16位,2字节需要使用htons。不可socklen_t len = sizeof(serveraddr);if (bind(_listensock, (struct sockaddr *)&serveraddr, len) < 0){std::cerr << "bind err" << std::endl;exit(-1);}std::cout << "bind success" << std::endl;// 3 设置sockfd为监听fdif (listen(_listensock, maxBacklog) < 0){std::cerr << "listen err" << std::endl;exit(-1);}std::cout << "listen success" << std::endl;}void run(){while (true){struct sockaddr_in clientaddr;memset(&clientaddr, 0, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int sockfd = accept(_listensock, (struct sockaddr *)&clientaddr, &len);if (sockfd < 0){std::cerr << "accept err" << std::endl;exit(-1);}std::cout << "accept success" << std::endl;// 到这里就能通过sockfd进行通信了// 通过clientaddr获取对方的ip/portstd::string clientip = inet_ntoa(clientaddr.sin_addr);uint16_t clientport = ntohs(clientaddr.sin_port);printf("获取连接成功,对方的ip/port为[%s][%d]", clientip.c_str(), clientport);// 执行响应的回调函数处理数据_callback(sockfd);// 关闭fdclose(sockfd);}}private:int _listensock;int _port;func_t _callback;};void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}printf("client --> server:%s\n", buffer);// 直接将数据返回给clientcount = send(sockfd, buffer, strlen(buffer), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}}}
2.2 tcpServer.cc
#include "tcpServer.hpp"
#include <iostream>
#include <memory>
using namespace std;// tcp 服务器,启动方式与udp server一样
//./tcpServer + local_port //我们将本主机的所有ip与端口绑定static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " lock_port\n\n";
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);}uint16_t serverport = atoi(argv[1]);unique_ptr<YZC::tcpServer> tsvr(new YZC::tcpServer(YZC::serviceIO, serverport));tsvr->init();tsvr->run();return 0;
}
至此,一个tcp服务器就实现了。我们编译运行一下

三. TcpClient
3.1 tcpClient.hpp
客户端不需要显示bind,也不需要监听listen和接收连接。只需要connect连接远端开始三次握手即可。
代码框架
#pragma once#include <iostream>
#include <string>#include <cstring>#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>const int NUM = 1024;namespace YZC
{using namespace std;class tcpClient{public:tpClient(const string &ip, const uint16_t &port): _sockfd(-1), _serverip(ip), _serverport(port) {}~tcpClient(){if (_sockfd > 0)close(_sockfd);}private:int _sockfd; // 套接字string _serverip; // 对方服务器ipuint16_t _serverport; // 对方服务器端口};
}
init初始化
void InitClient(){// 1.创建套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){cerr << "client creat socket error" << endl;}elsecout << "client creat socket success" << endl;// 2. bind udp客户端不需要显示bind,tcp客户端也不需要显示bind// client的port需要让客户端自行选择// 3.客户端需不需要listen? 客户端不需要监听连接// 4.客户打需不需要accept? 不需要accept// 5. 客户端需要什么? 需要发起连接}
run函数
void start(){// 5. 创建通信结构体,并填入连接对方的信息。然后使用connect发起连接struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) < 0){cerr << "client connect error" << endl;}else{// 6.连接成功,可以通信string msg;while (true){std::cout << "please enter:";getline(cin, msg); // 会自动清空\nsend(_sockfd, msg.c_str(), msg.size(), 0);char buffer[NUM];int n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){// 目前将读取的数据当前字符串buffer[n] = 0;cout << "server --> client" << buffer << endl;}else{// 说明服务端关闭了数据,我直接退出break;}}}}
3.2 tcpClient.cc
#include <iostream>
#include <memory>
#include "tcpClient.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage:\n\t" << proc << " serverip" << " serverport\n\n";
}// ./tcpClient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);std::unique_ptr<YZC::tcpClient> tcli(new YZC::tcpClient(serverip, serverport));tcli->init();tcli->start();return 0;
}
四. 代码测试
编译,运行,测试结果如下:

从下图可以看到,实现了client - server之间的通信

不过仍然有部分问题:如下图

如果让客户端1退出

可以看到,服务器死循环了,这是为啥呢?可以看到我们的代码


客户端关闭之后,recv返回0。服务器没有close关闭套接字fd,死循环打印数据。并且此时会处于一个 close_wait状态
所以:一定要记得使用完fd之后,需要关闭!
void serviceIO(int sockfd){// 这里仅做简单的数据收发while (true){char buffer[128] = {0};int count = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (count < 0){std::cerr << "recv err" << std::endl;exit(-1);}if (count == 0){//对方关闭break;}printf("client --> server:%s\n", buffer);// 直接将数据返回给clientcount = send(sockfd, buffer, strlen(buffer), 0);if (count < 0){std::cerr << "send err" << std::endl;exit(-1);}}close(sockfd);}
再次测试:

双方通信逻辑如下

如果有多个客户端同时发送数据如何解决?即如何解决客户端的并发问题?并且不同的客户端需要不同的连接,这些连接如何管理?
下篇文章我将详细介绍解决并发问题的多种方式

