当前位置:   article > 正文

C++Linux网络编程Day01_c++ linux网络编程day01

c++ linux网络编程day01

本文是我的学习笔记,学习路线跟随Github开源项目,链接地址:30dayMakeCppServer

最简单server程序

#include <iostream>

// sys(系统),socket(套接字),这个还是挺好理解的
#include <sys/socket.h>

#include <arpa/inet.h>

#include <stdio.h>
#include <string.h>

int main(){
	// 创建一个套接字描述符,这个描述符本质上就是一个Linux的文件描述符
	int socketfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in serv_addr;
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	 // s_addr就是用来存储32位IPV4地址的
	serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	// 开启服务器的8888端口,问题是这个hton是什么
	// hton是一个将主机字节序(host byte order)的端口号转换为网络字节序(network byte order)
	serv_addr.sin_port = htons(8888);

	/*
		bind首先绑定套接字的文件描述符
		后者将传入的serv_addr强制转换为一个sockaddr类型的指针
		也就是将sockaddr_in强制转换为sockaddr
		这是因为后者是通用套接字结构体,而前者是专属于IPv4的
		由此可知:网络套接字实际上是基于通用套接字的
	*/
	bind(socketfd, (sockaddr*)&serv_addr, sizeof(serv_addr));

	// 后者是一个常量,表示监听队列的最大长度
	listen(socketfd, SOMAXCONN);

	struct sockaddr_in clnt_addr;
	/*
		初始化一个socklen_t类型的变量,其值为clnt_addr的大小
		这个类型是一个无符号整型,用于表示套接字地址结构体的长度
	*/
	socklen_t clnt_addr_len = sizeof(clnt_addr);
	bzero(&clnt_addr, clnt_addr_len);

	int clnt_sockfd = accept(socketfd, (sockaddr*)&clnt_addr, &clnt_addr_len);

	/*
		inet_ntoa是将将一个 32 位的 IPv4 地址从网络字节序转换为点分十进制的IP地址字符串
		ntohs用于将网络字节序转换为主机字节序
		在这里就是将看不懂的内容转换为我们能看得懂的东西
	*/
	printf("new client fd %d IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

基本所有的知识点我都写在其中了,有些基础性的东西需要大家自己去学习”计算机网络“的相关知识,至于为什么在使用accept等函数的时候,需要将sockaddr_in转换成sockaddr,这点需要看书:游双的《Linux高性能服务器编程》,在其中的第五章第一节:socket地址API中有详细讨论。
接下来我们看看其中使用到的头文件:sys/inet.h和arpa/inet.h

sys/socket.h

这个头文件是网络编程的核心头文件之一,它包含了一些用于网络贬称搞到常量、数据结构和函数原型。这里指出一些常用的:

常量
  • AF_INET:表示IPv4地址族
  • AF_INET6:表示IPv6地址族
  • SOCK_STREAM:表示TCP套接字类型
  • SOCK_DGRAM:表示UDP套接字类型
  • SO_REUSEADDR:表示允许套接字地址重用
数据结构
  • sockaddr_in:用于储存IPv4地址和端口号的结构体
  • aockaddr_in6:用于存储IPv6地址和端口号的结构体
  • sockaddr:用于通用的套接字地址结构体
函数

与网络通信的一系列API都在其中:

  • socket():用于创建套接字
  • bind():用于将套接字绑定到指定的地址和端口
  • listen():用于监听套接字上的连接请求
  • accept():用于接收连接请求并创建新的套接字,用于与客户端通信
  • connect():用于发起连接请求,并与服务器建立连接
  • sned()/sendto():用于发送数据
  • recv()/recvfrom():用于接收数据
  • close():用于关闭套接字

这些函数并不是sys/socket.h中的全部,这里只写了比较基础的、常用的,还有很多的内容在netinet/in.h头文件中

arpa/inet.h

在上面,我们提到了头文件netinet/in.h,这个头文件包含在arpa/inet.h中,因此我们使用了这个头文件就可以不用再次包含。它也包含了许多网络通信相关的常量、结构体、函数。

常量
  • INADDR_ANY:表示任意地址,用于将套接字绑定到所有可用的网络接口
  • INADDR_LOOPBACK:表示回环地址,即:127.0.0.1,用于本地回环测试和通信
  • INADDR_BROADCAST:表示广播地址,用于向同一网络中的所有主机发送数据
数据结构
  • in_addr:IPv4地址的结构体,定义为struct in_addr。该结构体包含一个字段s_addr,用于存储32位的IPv4地址
函数
  • inet_addr():将一个表示IPv4地址的字符串转换为对应的32位无符号整数,返回网络字节序表示的IPv4地址
  • inet_addr():将一个32位无符号整数表示的IPv4地址转换为对应的点分十进制字符串表示
  • inet_pton():将一个IPv4或IPv6地址的字符串表示转换为对应的二进制格式,并储存到指定的地址结构体中
  • inet_ntop():将一个二进制格式的IPv4或IPv6地址转换为对应的字符串表示

第一个C/S应用

server.cpp:

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "util.h"

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    errif(sockfd == -1, "socket create error");

    struct sockaddr_in serv_addr;
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(8888);

    errif(bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket bind error");

    errif(listen(sockfd, SOMAXCONN) == -1, "socket listen error");
    
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_len = sizeof(clnt_addr);
    bzero(&clnt_addr, sizeof(clnt_addr));

    int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
    errif(clnt_sockfd == -1, "socket accept error");

    printf("new client fd %d! IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
    while (true) {
        char buf[1024];
        bzero(&buf, sizeof(buf));
        ssize_t read_bytes = read(clnt_sockfd, buf, sizeof(buf));
        if(read_bytes > 0){
            printf("message from client fd %d: %s\n", clnt_sockfd, buf);
            write(clnt_sockfd, buf, sizeof(buf));
        } else if(read_bytes == 0){
            printf("client fd %d disconnected\n", clnt_sockfd);
            close(clnt_sockfd);
            break;
        } else if(read_bytes == -1){
            close(clnt_sockfd);
            errif(true, "socket read error");
        }
    }
    close(sockfd);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

client.cpp:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "util.h"

int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    errif(sockfd == -1, "socket create error");

    struct sockaddr_in serv_addr;
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(8888);
    
    errif(connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1,
        "socket connect error");

    while(true){
        char buf[1024];
        bzero(&buf, sizeof(buf));
        scanf("%s", buf);
        ssize_t write_bytes = write(sockfd, buf, sizeof(buf));
        if(write_bytes == -1){
            printf("socket already disconnected, can't write any more!\n");
            break;
        }
        bzero(&buf, sizeof(buf));
        ssize_t read_bytes = read(sockfd, buf, sizeof(buf));
        if(read_bytes > 0){
            printf("message from server: %s\n", buf);
        }
        else if(read_bytes == -1){
            close(sockfd);
            errif(true, "socket read error");
        }
        else if(read_bytes == 0){
            printf("server socket disconnected!\n");
            break;
        }
    }
    close(sockfd);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

Makefile:

# shell和Makefile有点像
build:
	make client;
	make server

client: client.cpp util.cpp
	g++ -o client client.cpp util.cpp

server: server.cpp util.cpp
	g++ -o server server.cpp util.cpp

clean:
	rm -f server client
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

util.h:

#ifndef UTIL_H
#define UTIL_H

void errif(bool, const char*);

#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

util.cpp:

#include "util.h"
// #include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void errif(bool condition, const char* errmsg){
    if(condition){
        perror(errmsg);
        exit(EXIT_FAILURE);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

unistd.h

unistd.h 是C语言标准库中的一个头文件,它提供了对POSIX(可移植操作系统接口)的访问。其中包含了许多函数和符号常量,用于进程控制、文件操作、目录操作等方面的功能

函数
  • fork():创建一个新进程
  • exec()系列函数:用于执行新的程序文件。例如:execl(),execv(),execle()等
  • exit():使当前进程退出
  • sleep():使当前进程退出一段时间
  • access():检查文件的访问权限
  • chdir():改变当前工作目录
  • getpid():获取当前进程的PID
  • getuid():获取房前用户的用户ID
  • open():打开一个文件
  • close():关闭一个已打开的文件

由于在Linux中:”万事万物皆文件“,因此,我们操作进程间通信实际上就是对文件进行操作。
在上面的代码中:我们使用了该头文件中的writeread来进行网络接口的数据读写操作;使用了close来关闭网络连接文件、释放相关系统资源。

server和client需要做的操作

  • 服务端:
    1. 创建套接字socket
    2. bind()绑定IP地址,Port等相关信息,在本例中是使用的IPv4,因此使用的是sockaddr_in
    3. 开始监听listen
    4. 创建一个与客户端进行连接的socket,使用accept接收客户端的连接
    5. 使用read读取客户端传递的信息,并使用write向客户端返回信息
    6. 使用close断开连接(关闭套接字)
  • 客户端:
    1. 创建套接字socket
    2. 使用connect与服务端进行连接
    3. 使用write发送信息,使用read读取收到的信息
    4. 使用close关闭套接字

细节解析

阅读server.cpp(服务端)源码,我们能够发现:我们一开始创建的socket只用于监听,而在与客户端连接的时候我们并没有使用这个socket

	// bind绑定sockfd,并使其处于监听状态
    errif(bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket bind error");
    errif(listen(sockfd, SOMAXCONN) == -1, "socket listen error")


	// 使用accept接收客户端发来的连接请求
	int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可能我们需要看看accept的声明:

extern int accept (int __fd, __SOCKADDR_ARG __addr,
		   socklen_t *__restrict __addr_len);
  • 1
  • 2

可以发现它的返回值是个int类型,而Linux中,文件描述符也正好是个int类型,在查询资料后证实,accept其实是新开了个文件描述符,用于维持和客户端的通信。我们需要通过这个文件描述符进行网络通信。
套接字可以分为两种类型:监听套接字连接套接字,我们bind的就是监听套接字,accept所创建的文件描述符就是连接套接字,连接套接字才是真正用来网络通信的的

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/187208
推荐阅读
相关标签
  

闽ICP备14008679号