C++网络编程(一)本地socket通信

news2024/11/15 5:58:01

C++网络编程(一) socket通信

前言

本次内容简单描述C++网络通信中,采用socket连接客户端与服务器端的方法,以及过程中所涉及的函数概要与部分函数使用细节。记录本人C++网络学习的过程。

网络通信的Socket

socket,即“插座”,在网络中译作中文“套接字”,应用于计算机网络建立起数据连接。基于TCP/IP协议进行通信,将本地的ip地址与端口号相结合。两个程序可以通过各自的socket建立一个通道用于数据传输。

socket提供多种机制以供选择,但常用的一般为两种,即SOCK_STREAM(流)和SOCK_DGRAM(数据报)。

流socket基于TCP协议,其建立的通道是一个具有有序、可靠、全双工的字节流。

数据报socket基于UDP协议,其建立的通道传送数据是不可靠的,尽最大努力交付的,但传输速度较快。

目前在网络中使用的数据传送基本是基于TCP协议。

网络通信基本过程

请添加图片描述

首先建立起服务端,在服务端中采用socket()构建方法,建立服务端响应socket,其主要作用是用于接收由客户端socket发起的建立请求。随后bind()将创建的响应socket与本地的ip地址及设置的端口绑定。listen()为响应socket设置被动监听状态,监听客户端发来的建立请求。accept()代表一切就绪,此时服务器程序运行进入阻塞状态,等待客户端程序发来请求。

客户端中同样需要先建立起客户端socket,该socket所采用的协议应当与服务器端socket相同。客户端socket不需要绑定本地ip与端口,但应当指明所连接的服务器端的ip地址与端口号,通过connet()主动发起连接请求,与服务器端socket相连。

服务器端accept()程序收到客户端socket发来的请求后,会立即创建一个新的服务socket,与客户端的socket相对应,用于数据的收发与传送。即,服务器端会有两个socket,而客户端仅有一个socket

当双方数据交互结束后,各自调用close(),释放掉本地的socket。

代码示例

//server.cpp
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cerrno>

#define MY_PORT 1234//端口号
#define BUF_SIZE 1024//最大缓存
#define QUEUE 20//最大连接数

int main(){
    int server_sockfd = socket(AF_INET,SOCK_STREAM,0);//建立响应socket

    struct sockaddr_in server_sockaddr;//保存本地地址信息
    server_sockaddr.sin_family = AF_INET;//采用ipv4
    server_sockaddr.sin_port = htons(MY_PORT);//指定端口
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);//获取主机接收的所有响应

    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){//绑定本地ip与端口
        perror("Bind Failure\n");
        printf("Error: %s\n",strerror(errno));//输出错误信息
        return -1;
    }
    printf("Listen Port : %d\n",MY_PORT);
    if(listen(server_sockfd,QUEUE) == -1){//设置监听状态
        perror("Listen Error");
        return -1;
    }
    char buffer[BUF_SIZE];//一次传输的数据缓存
    struct sockaddr_in client_addr;//保存客户端地址信息
    socklen_t length = sizeof(client_addr);//需要的内存大小

    printf("Waiting for connection!\n");

    int connect_fd = accept(server_sockfd,(struct sockaddr *)&client_addr,&length);//等待连接,返回服务器端建立连接的socket
    if(connect_fd == -1){//连接失败
        perror("Connect Error");
        return -1;
    }
    printf("Connection Successful\n");
    while(1){//数据收发与传输
        memset(buffer,0,sizeof(buffer));
        int len = recv(connect_fd,buffer,sizeof(buffer),0);//接收数据
        // input "exit" or runtime error
        if(strcmp(buffer,"exit\n")==0 ||len <= 0) break;
        printf("client send message: %s",buffer);
        strcpy(buffer,"successful");
        send(connect_fd,buffer,strlen(buffer),0);//发送数据
        printf("send message: %s\n",buffer);
    }
    close(connect_fd);//关闭数据socket
    close(server_sockfd);//关闭响应socket
    return 0;
}
//client.cpp
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cerrno>

#define MY_PORT 1234
#define BUF_SIZE 1024
#define SERVER_IP "127.0.0.1"//服务端ip,这里是本机

int main(){

    int client_sockfd = socket(AF_INET,SOCK_STREAM,0);//建立客户端socket

    struct sockaddr_in servaddr;//保存服务器端地址信息
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;//ipv4协议
    servaddr.sin_port = htons(MY_PORT);//端口
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);//ip地址

    printf("connect to %s:%d\n",SERVER_IP,MY_PORT);
    int connect_fd = connect(client_sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//建立连接
    if(connect_fd<0){
        perror("Connect Error");
        _exit(1);
    }
    printf("Connect Successful\n");
    char sendbuf[BUF_SIZE];
    char recvbuf[BUF_SIZE];
    while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL){//数据传送
        memset(recvbuf,0,sizeof(recvbuf));
        printf("send message:%s",sendbuf);
        send(client_sockfd,sendbuf,strlen(sendbuf),0);
        if(strcmp(sendbuf,"exit\n")==0) break;
        int len = recv(client_sockfd,recvbuf,sizeof(recvbuf),0);
        if(len<=0){
            printf("receive failure");
            break;
        }
        printf("recv message:%s\n",recvbuf);
        memset(sendbuf,0,sizeof(sendbuf));
    }
    close(client_sockfd);//关闭客户端socket
    return 0;
}

函数分析

主机字节序和网络字节序

主机字节序即在计算机内部存储数据的格式,通常为小端序,即低地址存放低字节的内容,比如说一个ipv4的地址,当其作为数字存储时,一共需要四个字节来保存值,对于"127.0.0.1"来说,主机字节序中,最低位的字节保存了127,最高位则是1。

网络字节序是TCP/IP中规定好的一种数据格式,采用大端序,即高地址存放低字节的内容,同样对于"127.0.0.1"来说,网络字节序中,最低位的字节保存了1,最高位则是127。

字节地址0123
主机字节序01111111000000000000000000000001
网络字节序00000001000000000000000001111111
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);//主机转网络
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);//网络转主机
unsigned short int ntohs(unsigned short int netshort);

htonl表示host to network long,依次类推

INADDR_ANY其值为"0.0.0.0",其意义便是不监听特定的ip,而接收所有通过指定端口发送至本机的信号。

Socket地址

通用地址

#include <bits/socket.h>//<sys/socket.h>头文件包含了此头
struct sockaddr
{
    sa_family_t sa_family; //地址族类型
    char sa_data[14]; //地址值
}

专用地址

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6,此处仅列述IPv4。

#include <netinet/in.h>
struct sockaddr_in
{
    sa_family_t sin_family;/*地址族:AF_INET*/
    u_int16_t sin_port;/*端口号,要用网络字节序表示*/
    struct in_addr sin_addr;/*IPv4地址结构体,见下面*/
    unsigned char sin_zero[8];/*为了保持与struct sockaddr长度相同*/
};

struct in_addr
{
    u_int32_t s_addr;/*IPv4地址,要用网络字节序表示*/
};

在实际使用时,sockaddr_in需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。

IP地址转换

#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。

inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。

inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。

注:以上函数仅用于IPv4的地址转换。

创建socket

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
/*
domain参数:告诉系统使用哪个底层协议族
type参数:指定服务类型
protocol参数:在前两个参数构成的协议集合下,再选择一个具体的协议
*/

参数通常取值:

domain:PF_INET(Protocol Family of Internet,用于IPv4)、PF_INET6(用于IPv6)

type:SOCK_STREAM(流,TCP)、SOCK_DGRAM(数据报,UDP)

protocol:0(默认协议)

当socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

绑定IP地址和端口

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。

bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCES和EADDRINUSE,它们的含义分别是:

  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
  • EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。

监听socket

#include <sys/socket.h>
int listen(int sockfd, int backlog);
/*
sockfd: 指定被监听的socket
backlog: 设置内核监听队列的最大长度
*/

监听队列:即处于向服务端socket发出连接请求,但TCP建立三次握手中尚未进行第二次握手的序列。

监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。

listen成功时返回0,失败则返回-1并设置errno。

注:listen()函数执行成功后,客户端发来连接请求,在经过listen后,TCP的三次握手已经完成,建立了连接。

接收连接

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
/*
sockfd:是执行过listen系统调用的监听socket
addr:用来获取被接受连接的远端socket地址
addrlen:指出远程socket的长度
返回值:一个新的连接socket
*/

函数调用成功返回的socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。

调用失败则返回-1并设置errno。

发起连接

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);

sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。

connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:

  • ECONNREFUSED,目标端口不存在,连接被拒绝。
  • ETIMEDOUT,连接超时。

关闭连接

#include <unistd.h>
int close(int fd);//fd是待关闭的socket

close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。类似于Linux系统中的硬链接数。比方说在多进程程序中,父进程和子进程都对一个socket操作,只有在所有进程中都调用close()关闭该socket,该socket才会被关闭。

有一个函数可以立即终止连接:

#include <sys/socket.h>
int shutdown(int sockfd,int howto);

sockfd参数是待关闭的socket。howto参数决定了shutdown的行为,可取值如下:

  • SHUT_RD 关闭读
  • SHUT_WR 关闭写
  • SHUT_RDWR 关闭读写

数据读写

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);

此处函数仅针对TCP流数据读写,对UDP数据报的读写暂且不表。

recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags通常设置为0即可。recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。

send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。

网络信息API

#include <netdb.h>
struct hostent*gethostbyname(const char* name);//name:指定目标主机的主机名
struct hostent*gethostbyaddr(const void* addr, size_t len, int type);
/*
addr:指定目标主机的IP地址
len:指定addr所指IP地址的长度
type:指定addr所指IP地址的类型(AF_INTE、AF_INET6)
*/

struct hostent
{
    char* h_name;/*主机名*/
    char** h_aliases;/*主机别名列表,可能有多个*/
    int h_addrtype;/*地址类型(地址族)*/
    int h_length;/*地址长度*/
    char** h_addr_list;/*按网络字节序列出的主机IP地址列表(数字形式,不是点分十进制)*/
};

gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。

//示例
struct hostent *res = gethostbyname("www.baidu.com");
if (res)
{
    for (int i = 0; res->h_addr_list[i]; i++)
        printf("IP addr %d: %s\n", i + 1, 
        inet_ntoa(*(struct in_addr *)res->h_addr_list[i]));
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399722.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目管理必备:如何绘制一份优秀的甘特图?

本文一共分为两部分—— 分享60Excel甘特图模板&#xff0c;简单省事儿分享两种甘特图制作教程&#xff0c;高效快捷 第一部分——60甘特图模板 分享一些项目管理甘特图的模板&#xff0c;省事儿&#xff01;高效&#xff01;简单&#xff01; Excel甘特图表模板自取&#xf…

Laya小游戏开发,laya3D美术篇——1——关于laya自带的几个shader的基础运用讲解。

最近三年&#xff0c;基本上做的都是laya小游戏项目。也就是微信小程序&#xff0c;很多业内同行都觉得laya做小游戏不好用&#xff0c;去用了其他平台&#xff0c;甚至还有些做app游戏的&#xff0c;都不来趟laya这个坑。原因有那么以下几点。laya对于unity的辅助开发&#xf…

STM32的USART发送数据时如何使用TXE和TC标志

8人从A出发去D旅游的故事 STM32的USART发送数据时如何使用TXE和TC标志 TXE为1&#xff1a; TDR里的数据全部移到了移位寄存器&#xff0c;且没有新的数据TDR。 TXE为0&#xff1a; TDR里有数据&#xff0c;未空&#xff0c;则TXE0。 TC为1&#xff1a; 从TDR过来的数据全部被移…

一文上手图数据备份恢复工具 NebulaGraph BR

作者&#xff1a;NebulaGraph 工程师 Kenshin NebulaGraph BR 开源已经有一段时间了&#xff0c;为了给社区用户提供一个更稳、更快、更易用的备份恢复工具&#xff0c;去年对其进行了比较大的重构。NebulaGraph BR&#xff08;以下简称 BR&#xff09;分为社区版和企业版两个版…

Git学习笔记(五)——分支

一、创建与合并分支创建分支&#xff1a;Git创建一个分支很快&#xff0c;因为除了增加一个指针&#xff0c;改改HEAD的指向&#xff0c;工作区的文件都没有任何变化。合并分支&#xff1a;就是直接把master&#xff08;其中一条分支&#xff09;指向dev&#xff08;另一条分支…

python基础条件循环语句

1、编写代码完成一个名片显示,要求使用取消换行和格式化操作符 # 编写代码完成一个名片显示,要求使用取消换行和格式化操作符name 张三 qq 1234567 phone_num 1234567 com_address 北京print(\n 姓名:%s\n qq:%d\n 手机号:%d\n 公司地址:%s\n %(name ,qq ,phone_num, com_…

忆享科技优化入职培训加强人效管理全面迎接新挑战

-优化入职培训-忆享科技加强人效管理入职培训课程 ✦ KPI系统上线 ✦ 砥砺前行前言许多企业随着自身的不断发展&#xff0c;对于各类人才引进需求也越来越迫切&#xff0c;一批批新员工的加入&#xff0c;公司规模逐渐扩大&#xff0c;给公司带来了全新的血液。但在大量新员工加…

jsPlumb Components Crack

jsPlumb Components Crack 为支持Vue 2&#xff0c;所有组件都添加了包装器。 已为所有组件添加了包装器以支持Svelte。 改进了在流程图生成器中编辑多个选定节点。 jsPlumb组件是一组可嵌入的组件&#xff0c;可将可视连接快速集成到网页中。jsPlumb组件基于jsPlumb Toolkit库…

lesson8-Linux多线程

Linux线程概念 线程在进程内部执行,是OS调度的基本单位OS是可以做到让进程进行资源的细粒度划分的物理内存是以4kb为单位的我们的.exe可执行程序本来就是按照地址空间的方式进行编译的页表映射 - 详细图 理解线程 线程在进程的地址空间内运行, 进程内部具有多个执行流的,而线程…

【java基础】集合基础说明

文章目录基本介绍Collection接口Iterator和Iterable接口Map接口关于Iterator接口的一些说明框架中的接口具体集合总结基本介绍 集合就是存储用来存储一系列数据的一种数据结构。在这篇文章中会介绍集合的一些基本概念。 Collection接口 集合的基本接口是Collection接口&…

Metabase和Superset 对比分析

Metabse中文社区Metabase和Superset都是排名靠前的开源的数据可视化软件&#xff0c;在技术上有许多相似之处。他们的比较可以帮助用户选择更好的开源平台进行数据可视化。关于Superset 编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;Super…

Golang反射源码分析

在go的源码包及一些开源组件中&#xff0c;经常可以看到reflect反射包的使用&#xff0c;本文就与大家一起探讨go反射机制的原理、学习其实现源码 首先&#xff0c;了解一下反射的定义&#xff1a; 反射是指计算机程序能够在运行时&#xff0c;能够描述其自身状态或行为、调整…

智慧赋能,聚力开源——第四届OpenI/O 启智开发者大会开源治理专场顺利举办!

为汇聚国内外知名开源组织共同探讨中国开源生态建设及开源治理相关议题&#xff0c;推进产学研用开源合作&#xff0c;2月24日下午&#xff0c;第四届OpenI/O启智开发者大会在深圳人才研修院智汇中心举办以“构建开源联合体&#xff0c;共建开源生态”为主题的开源治理专场分论…

C++基础了解-17-C++日期 时间

C日期 & 时间 一、C日期 & 时间 C 标准库没有提供所谓的日期类型。C 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构&#xff0c;需要在 C 程序中引用 头文件。 有四个与时间相关的类型&#xff1a;clock_t、time_t、size_t 和 …

opencv识别车道线(霍夫线变换)

目录1、前言2、霍夫线变换2.1、霍夫线变换是什么&#xff1f;2.2、在opencv中的基本用法2.2.1、HoughLinesP函数定义2.2.2、用法3、识别车道3.1、优化3.1.1、降噪3.1.2、过滤方向3.1.3、截选区域3.1.4、测试其它图片图片1图片2图片31、前言 最近学习opencv学到了霍夫线变换&am…

ruoyi对接CAS统一身份认证

暂定逻辑如下&#xff1a;搭建CAS服务器端&#xff1a;项目地址&#xff1a;https://gitee.com/weigang_wu/cas-server-webapp.git项目里有二开的说明文档&#xff0c;如&#xff1a;按照自定义的数据库校验修改如下&#xff1a;首先&#xff1a;修改数据库连接以及查询数据这里…

博客系统(前后端分离版)

博客系统的具体实现 文章目录博客系统的具体实现软件开发的基本流程具体实现的八大功能数据库设计创建数据库操作数据库引入依赖封装DataSource创建实体类将JDBC增删改查封装起来实现博客列表页web.xml的配置文件实现博客系统的展示功能登录功能强制要求用户登录显示用户信息退…

求职复盘:干了四年外包出来,面试5次全挂

我的情况 大概介绍一下个人情况&#xff0c;男&#xff0c;毕业于普通二本院校非计算机专业&#xff0c;18年跨专业入行测试&#xff0c;第一份工作在湖南某软件公司&#xff0c;做了接近4年的外包测试工程师&#xff0c;今年年初&#xff0c;感觉自己不能够再这样下去了&…

为什么做知识管理,就想选择Baklib呢?

随着科技的不断发展&#xff0c;知识管理已经成为现代企业不可或缺的一个重要组成部分。由于信息化快速发展&#xff0c;企业每天都会产生大量的数据和信息&#xff0c;如何高效地获取、整理和利用这些信息已经成为了企业成功的关键因素之一。为了更好地管理企业知识&#xff0…

利用Iptables构建虚拟路由器

利用Iptables构建虚拟路由器 &#xff08;1&#xff09;修改网络类型 在VMware Workstation软件中选择“编辑→虚拟网络编辑器”菜单命令&#xff0c;在虚拟网络列表中选中VMnet1&#xff0c;将其配置为“仅主机模式&#xff08;在专用网络内连接虚拟机&#xff09;”&#x…