当前位置: 首页 > news >正文

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);}

再次测试:

双方通信逻辑如下

        如果有多个客户端同时发送数据如何解决?即如何解决客户端的并发问题?并且不同的客户端需要不同的连接,这些连接如何管理?

        下篇文章我将详细介绍解决并发问题的多种方式

http://www.dtcms.com/a/606486.html

相关文章:

  • 镇江大港南站wordpress 新闻 通知
  • 【ZeroRange WebRTC】RFC 5389:STUN 协议规范(中文整理与译注)
  • 苏州网站制作及推广二手闲置平台网站怎么做
  • Unity零碎物体合并为一个整体mesh
  • 做响应式网站图片需要做几版在哪个网站找学做包子
  • 国内大型php网站建设商务网站开发背景
  • HarmonyOS开发-ArkWeb开发指导
  • kotlin:if、when语句介绍
  • 扩展函数练习题
  • Java后端常用技术选型 |(一)数据库篇
  • 婚纱摄影网站应该如何做优化室内装修设计软件下载
  • 从零开始写算法——二分-寻找旋转排序数组中的最小值
  • 一站式网站建设与运营简述网站开发平台及常用工具
  • 易语言程序反编译 | 深入了解反编译技术与应用
  • 本地部署数据库管理工具 NocoDB 并实现外部访问(Linux 版本)
  • 9V-36V转3.3V4A同步降压WT6043A
  • P10668 BZOJ2720 [Violet 5] 列队春游(自己加强版) 题解
  • 学做立体书的网站wordpress小人插件
  • 网站图片地址怎么做的搜索指数的数据来源是什么
  • c# 上位机作为控制端与下位机通信方式
  • 一文了解UI自动化测试
  • 使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第三十六讲)
  • MySQL 缓存机制与查询缓存的消亡史
  • 平凡前端之路_19.数组的扩展
  • 做校园网站黑龙江建设网ca锁
  • 俞润装饰做哪几个网站杭州萧山网络
  • 平安车险官方保险网站搜索引擎网址有哪些
  • 视频sdk是什么意思?
  • CSDN在干啥?
  • 自查C语言水平