【在Linux世界中追寻伟大的One Piece】Socket编程TCP

ops/2024/10/31 21:02:31/

目录

1 -> TCP socket API

2 -> V1 -Echo Server

2.1 -> 测试多个连接的情况


1 -> TCP socket API

socket():

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
  • 应用程序可以像读写文件一样用read/write在网络上收发数据。
  • 如果socket()调用出错则返回-1。
  • 对于IPv4,family参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • protocol参数的介绍从略,指定为0即可。

bind():

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号。
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
  • struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

我们的程序中对myaddr参数是这样初始化的:

  1. 将整个结构体清零。
  2. 设置地址类型为AF_INET。
  3. 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
  4. 端口号为SERV_PORT,定义为9999。

listen():

  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5)。
  • listen()成功返回0,失败返回-1。

accept():

  • 三次握手完成后,服务器调用accept()接受连接。
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
  • 如果给addr参数传NULL,表示不关心客户端的地址。
  • addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

我们的服务器程序结构是这样的:

connect

  • 客户端需要调用connect()连接服务器。
  • connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
  • connect()成功返回0,出错返回-1。

2 -> V1 -Echo Server

nocopy.hpp

#pragma once
#include <iostream>class nocopy
{
public:nocopy() {}nocopy(const nocopy&) = delete;const nocopy& operator = (const nocopy&) = delete;~nocopy() {}
};

TcpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"const static int default_backlog = 6; // TODO
class TcpServer : public nocopy
{
public:TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 都是固定套路void Init(){// 1. 创建 socket, file fd, 本质是文件_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "create socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));exit(Fatal);}int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR |SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success,sockfd: % d\n", _listensock);// 2. 填充本地网络信息并 bindstruct 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);// 2.1 bindif (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n",_listensock);// 3. 设置 socket 为监听状态,tcp 特有的if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success,sockfd: % d\n", _listensock);}// Tcp 连接全双工通信的.void Service(int sockfd){char buffer[1024];// 一直进行 IOwhile (true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "client say# " << buffer <<std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(),echo_string.size());}else if (n == 0) // read 如果返回值是 0,表示读到了文件结尾(对端关闭了连接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));break;}}}void Start(){_isrunning = true;while (_isrunning){// 4. 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0){lg.LogMessage(Warning, "accept socket error, errnocode: % d, error string : % s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get n newsockfd: % d\n", sockfd);// 5. 提供服务, v1~v4// v1// Service(sockfd);close(sockfd);}}~TcpServer(){}
private:uint16_t _port;int _listensock; // TODObool _isrunning;
};

Comm.hpp

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>enum 
{Usage_Err = 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

TcpClient.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
using namespace std;void Usage(const std::string& process)
{std::cout << "Usage: " << process << " server_ip server_port"<< std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{if (argc != 3){Usage(argv[0]);return 1;}std::string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 1. 创建 socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;return 1;}// 2. 要不要 bind?必须要有 Ip 和 Port, 需要 bind,但是不需要用户显示的 bind,client 系统随机端口// 发起连接的时候,client 会被 OS 自动进行本地绑定// // 2. connectstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// p:process(进程), n(网络) -- 不太准确,但是好记忆inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 1. 字符串 ip->4 字节 IP 2. 网络序列int n = connect(sockfd, CONV(&server), sizeof(server)); // 自动进行 bind 哦!if (n < 0){cerr << "connect error" << endl;return 2;}// 并没有向 server 一样,产生新的 sockfd.未来我们就用 connect 成功的sockfd 进行通信即可.while (true){string inbuffer;cout << "Please Enter# ";getline(cin, inbuffer);ssize_t n = write(sockfd, inbuffer.c_str(),inbuffer.size());if (n > 0){char buffer[1024];ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;cout << "get a echo messsge -> " << buffer <<endl;}else if (m == 0 || m < 0){break;}}else{break;}}close(sockfd);return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。

注意:

  • 客户端不是不允许调用bind(),只是没有必要显示的调用bind()固定一个端口号。否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。
  • 服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

2.1 -> 测试多个连接的情况

再启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。

分析原因,是因为我们accecpt了一个请求之后,就在一直while循环尝试read,没有继续调用到 accecpt,导致不能接受新的请求。

我们当前的这个TCP,只能处理一个连接,这是不科学的。


感谢各位大佬支持!!!

互三啦!!!


http://www.ppmy.cn/ops/129951.html

相关文章

一个vue3的待办列表组件

一个vue3的待办列表组件, 仿企业微信的待办列表 TodoList.vue <template><div><el-input v-model"todoInput" placeholder"写下你的待办事项..." class"el-input" keyup.enter"addTodo"input-style"background-c…

Chromium HTML Input 类型password 对应c++

一、密码字段&#xff1a; 密码字段通过标签 <input type"password"> 来定义: <form> Password: <input type"password" name"pwd"> </form> 二、password 对应c定义&#xff1a; third_party\blink\renderer\core\…

VR动捕数据手套如何配合头显装置进行机器臂遥操作?

早期的遥操作机器人主要通过直接控制结构实现&#xff0c;用户通过主机通讯接口直接控制机器人的运动。然而&#xff0c;随着应用场景的复杂化&#xff0c;直接控制结构的局限性逐渐显现。进入21世纪&#xff0c;随着5G、AI和VR等新兴技术的融入&#xff0c;遥操作机器人变得更…

记录一个跳跃的小游戏

记录一个跳跃的小游戏 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevic…

Lua语法基础全面剖析(中篇)

接上篇&#xff0c; 今天我们来进行Lua语法全面剖析。 由于篇幅过长&#xff0c;将分为上中下三篇进行讲解&#xff0c;本篇为中篇。 6.字符串 6.1 string 类型变量 字符串&#xff08;即string&#xff09;&#xff0c;就是一串文本数据&#xff0c;可以存储你要的文本。 …

前端代码注释

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言类注释属性注释函数注释函数参数注释解构 & 函数返回结果 注释Vue Props 注释注释建议注释内容要清晰简洁注释类型避免不必要的注释采用一致的风格版本与更…

PHP+REDIS设置请求限流(设置1秒内最大请求数1000QPS)

双十一期间要做活动&#xff0c;设置商品请求限流&#xff0c;护航秒杀活动正常进行&#xff01;#设置1秒内最多同时1000请求 $maxNum 1000; $redisKey GoldMall:Huodong:gid.$g_id._.date(s); $onlineNum (int)$this->redis->get($redisKey); if($onlineNum){$online…

ToDesk远程连接Ubuntu无法载入桌面

仅用于记录自己解决Ubuntu下ToDesk无法打开桌面。 问题描述 Ubuntu的ToDesk可以远程其他设备&#xff0c;一切正常&#xff1b; 其他设备远程Ubuntu时&#xff0c;卡在“正在链接100%”界面&#xff0c;但无法显示。 除此之外&#xff0c;Ubuntu下打开ToDesk客户端&#xff…