TCP/IP网络编程-C++(上)

发布于:2024-11-29 ⋅ 阅读:(33) ⋅ 点赞:(0)

TCP/IP网络编程-C++ (上)

一、基于TCP的服务端/客户端

  • 先给出server端和client端代码,client向server发送请求,server接受请求并回复client消息,这里简单回复"hello world!",后面详细说明函数作用

1、server端代码

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

int main(void){
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);  
    if(serv_sock == -1)
        std::cout<<"socket error\n";

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"bind error\n";

    if(listen(serv_sock, 3) == -1)
        std::cout<<"listen error\n";

    struct sockaddr_in clie_addr;
    memset(&clie_addr, 0, sizeof(clie_addr));
    socklen_t clie_addr_size = 0;
    int clie_sock = accept(serv_sock, (struct sockaddr*) &clie_addr, &clie_addr_size);
    if(clie_sock == -1)
        std::cout<<"accept error\n";

    std::string message = "hello world!";
    send(clie_sock, message.c_str(), message.size(), 0);

    close(clie_sock);
    close(serv_sock);
    return 0;
}

2、client端代码

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

int main(void){
    int clie_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(clie_sock == -1)
        std::cout<<"socket error\n";

    std::string server_ip = "127.0.0.1";
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
    serv_addr.sin_port = htons(8080);

    if(connect(clie_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"connect error\n";

    char message[30] = {0};
    if(recv(clie_sock, message,30, 0) == -1)
        std::cout<<"read error\n";

    std::string str_message(message);
    std::cout<<"message : "<<str_message<<"\n";
    close(clie_sock);
    return 0; 
}

  • 可以大概看看,不理解没关系,接下来会解释每一个函数的作用

3、socket() 函数

3.1、函数原型

	socket(int domain, int type, int protocol); 
	// 成功会返回文件描述符,失败返回-1

3.2、参数解析

  • domain-协议族:套接字中使用的协议族信息。
  • type-套接字类型:套接字数据传输类型信息。
  • protocol-最终使用的协议:计算机间通信中使用的协议信息。
3.2.1、协议族(domain参数)
  • 协议族就是协议的分类信息,在<sys/socket.h>中声明的协议分类信息如下表:
名称 协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族
3.2.2、套接字类型(type参数)
  • 套接字类型指的是套接字数据传输方式,这里介绍两种类型,面向连接的套接字(TCP)面向消息的套接字(UDP)
  • 1、SOCK_STREAM:表示使用面向连接的套接字,提供可靠的、按序传递的、基于自己的服务。
  • 2、SOCK_DGRAM:表示使用面向消息的套接字,提供不可靠的、不按序传递的、以数据高速传输为目的的服务。
3.2.3、最终使用的协议(protocol参数)
  • 最终使用的协议指的是同一协议族中存在多个数据传输方式相同的协议,在同一协议族中数据传输方式相同,但使用的协议不同,一般该参数传递0即可。

4、struct sockaddr_in IPv4地址结构体

4.1、结构体原型

struct sockaddr_in
{
	sa_family_t		sin_family;		// 地址族
	uint16_t		sin_port;		// 16位TCP/UDP端口号
	struct in_addr	sin_addr;		// 32位IP地址
	char			sin_zero[8];	// 不使用
}
// struct in_addr原型
struct in_addr
{
	In_addr_t	s_addr;		// 32位IP地址
}

4.2、结构体成员分析

  • sin_family:每种协议使用的地址族均不同,比如IPv4使用4字节地址族,IPv6使用16字节地址族。常用的地址族如下表:
地址族 含义
AF_INET IPv4网络中使用的地址族
AF_INET6 IPv6网络中使用的地址族
AF_LOCAL 本地通信中采用的UNIX协议的地址族
  • sin_port:保存16位端口号
  • sin_addr:保存32位IP地址信息
  • sin_zero:没有特殊含义,为了使得sockaddr_in与sockaddr大小保持一致而插入的成员,(sockaddr后面介绍),填充位0即可

4.3、struct sockaddr 结构体

  • 在调用bind()函数时是将sockaddr_in转换为sockaddr进行传入的,因为bind()函数要求的参数类型是sockaddr,但为什么不直接定义sockaddr进行传入呢,而是定义sockaddr_in转换为sockaddr呢?如下sockaddr结构体原型:
struct sockaddr
{
	sa_family_t		sin_family;		// 地址族
	char 			sa_data[14];	//地址信息
}
  • 因为最终IP地址信息和端口号会保存到sa_data成员中,直接想sa_data中填写信息比较麻烦,所以就有了sockaddr_in,使用起了更方便。

5、字节序转换

  • 由于CPU存储数据分为大端序和小端序,但是网络数据传输中同一使用大端序,所以下面介绍字节序转换函数
  • (1)uint32_t ntohl (uint32_t __netlong):将uint32_t类型数据从网络字节序转换为主机字节序
  • (2)uint16_t ntohs (uint16_t __netshort):将uint16_t类型数据从网络字节序转换为主机字节序
  • (3)uint32_t htonl (uint32_t __hostlong):将uint32_t类型数据从主机字节序转换为网络字节序
  • (4)uint16_t htons (uint16_t __hostshort):将uint16_t类型数据从主机字节序转换为网络字节序
  • 只需要在想sockaddr_in结构体填充数据时需要进行转换,其它情况不需要转换字节序,这都是自动完成的。当然sockaddr_in中的sin_family也不需要进行字节序转换,因为sin_family 并不会发送到网络上(详细信息自行了解)。

6、bind() 函数

6.1、字符串IP与网络字节序互相转换

6.1.1、inet_addr()
  • 作用:将字符串类型IP转换为转换为整数并返回,示例如下:
#include <arpa/inet.h>
#include <string>
int main(void)
{
	std::string str_ip = "127.0.0.1";
	uint32_t u_ip = inet_addr(str_ip.c_str());	// 返回的结果u_ip可以直接赋值给sockaddr_in中的sin_addr.s_addr
	if(u_ip == INADDR_NONE){
		std::cout<<"无效ip"<<std::endl;
	}
	return 0;
}
6.1.2、 inet_aton()
  • 作用:将字符串类型IP转换为转换为整数同时将结果直接写入sockaddr_in中的sin_addr,调用成功返回true(1),失败返回false(0),示例如下:
#include <arpa/inet.h>
#include <string>
int main(void)
{
	std::string str_ip = "127.0.0.1";
	struct sockaddr_in addr;
	int ret = inet_aton(str_ip.c_str(), &addr.sin_addr);	
	if(ret == 0){
		std::cout<<"无效ip"<<std::endl;
	}
	return 0;
}
6.1.3、inet_ntoa()
  • 作用:将网络字节序的IP转换为字符串,成功是返回字符串IP,失败是返回-1,示例如下:
#include <arpa/inet.h>
#include <string>
int main(void)
{
	struct sockaddr_in addr;
	addr.sin_addr.s_addr = htonl(0x1020304);
	std::string str_ip = inet_ntoa(addr.sin_addr);	
	std::cout<<str_ip<<std::endl;
	return 0;
}

6.2 向套接字分配网络地址bind()函数

6.2.1、函数原型:
int bind (int __fd, const struct sockaddr * __addr, socklen_t __len);
//	成功返回0,失败返回-1
6.2.2、参数解析:
参数 参数说明
__fd 要分配地址信息的套接字文件描述符,socket()函数返回的文件描述符
__addr struct sockaddr_in IPv4地址结构信息
__len 参数2的长度

7、listen() 函数 - 进入等待连接请求状态

7.1 函数原型:

	int listen(int sock, int backlog);
	//	成功时返回0, 失败时返回-1

7.2 参数解析:

参数 参数说明
sock 套接字文件描述符
backlog 连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列
  • 所谓的等待连接请求状态是指客户端请求连接时,在受理连接前一直使请求处于等待连接状态

8、accept() 函数 - 受理客户端连接请求

8.1 函数原型:

	int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
	// 成功时返回套接字文件描述符,失败时返回-1

8.2 参数解析

参数 参数说明
sock 套接字文件描述符
addr 保存发起连接请求的客户端地址信息
addrlen 保存第二个参数的地址长度

9、connect()函数 - 向服务端发送连接请求

9.1 函数原型:

	int connect(int sock, struct sockaddr* servaddr, socklen_t* addrlen);
	//	成功时返回0,失败时返回-1

9.2 参数解析

参数 参数说明
sock 客户端套接字文件描述符
servaddr 目标服务器端地址信息
addrlen 第二个参数的长度

9.3 客户端地址信息在哪里

  • 客户端调用 connect() 函数函数后,发生一下两种情况才会完成函数调用:
    (1)、服务端接收到连接请求
    (2)、发生断网等异常情况而中断连接请求
  • 需要注意的是,连接请求并不意味着服务端调用 accept() 函数,其实是服务端把连接请求信息记录在等待队列中,因此 connect() 函数返回后并不立即进行数据交换。
  • 实现服务端必经过程之一就是给套接字分配IP和端口号,但在客户端实现过程中并没有为套接字分配IP和端口号,而是在创建套接字后就立即调用了connect()函数。难道客户端套接字不需要IP和端口号吗?当然不是,学过计算机网络都知道网络中数据交换必须要IP和端口号,既然如此,那客户端套接字何时?何地?如何分配地址呢?
    (1)、何时:在调用connect()函数时
    (2)、何地:操作系统内核中自动完成
    (3)、如何:IP使用主机的IP,端口随机分配
  • 因此,客户端IP和端口号是在调用connect()函数时自动分配,无需使用bind()函数进行分配。

10、基于TCP的服务端、客户端实现字符串转换

  • 我们已经学完了基础函数,前面我们给出的示例是很简单的调用流程,即客户端发送请求,服务端接受请求并发送了“hello world”的信息,然后各自关闭了连接。那服务端如何循环的接收并处理客户端的请求呢?客户端如何不断的向服务端发送请求呢?接下里我们实现一个简单的字符串转换功能,即客户端向服务端发送字符串,服务端接收到字符串后将字符串中大写字符转换为小写并返回给客户端。

10.1 客户端代码实现:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
const int BUFFER_SIZE = 128;

int main(void){
    int clie_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(clie_sock == -1)
        std::cout<<"socket error\n";

    std::string server_ip = "127.0.0.1";
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
    serv_addr.sin_port = htons(8080);

    if(connect(clie_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"connect error\n";

    while(true){
        std::string str_input = "";
        std::cout<<"please input:";
        std::cin>>str_input;
        if(str_input == "q")
            break;

        send(clie_sock, str_input.c_str(), str_input.size(), 0);
        
        char message[BUFFER_SIZE] = {0};
        int recv_size = 0;
        if((recv_size = recv(clie_sock, message, BUFFER_SIZE - 1, 0)) == -1)
            std::cout<<"read error\n";
        message[recv_size] = '\0';
        std::string str_message(message, recv_size);
        std::cout<<"before:"<<str_input<<"\n";
        std::cout<<"later:"<<str_message<<"\n";
    }
    close(clie_sock);
    return 0; 
}

10.2 服务端代码实现:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
#include <regex>
#include <cctype>
const int BUFFER_SIZE = 128;

void to_lower(const std::string& str_input, std::string& str_output){
    std::regex pattern("^[a-zA-Z]+$");
    bool is_letters = std::regex_match(str_input, pattern);
    if(is_letters){
        str_output.resize(str_input.size());
        std::transform(str_input.begin(), str_input.end(), str_output.begin(), ::tolower);
    }else{
        str_output = "包含其它字符,转换失败!";
    }
    return;
}

int main(void){
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);  
    if(serv_sock == -1)
        std::cout<<"socket error\n";

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"bind error\n";

    if(listen(serv_sock, 3) == -1)
        std::cout<<"listen error\n";

    struct sockaddr_in clie_addr;
    memset(&clie_addr, 0, sizeof(clie_addr));
    socklen_t clie_addr_size = 0;
    int clie_sock = 0;
    clie_sock = accept(serv_sock, (struct sockaddr*) &clie_addr, &clie_addr_size);
    if(clie_sock == -1)
        std::cout<<"accept error\n";
    while(true){
        char mess[BUFFER_SIZE] = {0};
        int recv_size = 0;
        if((recv_size = recv(clie_sock, mess, BUFFER_SIZE - 1, 0)) == -1) {
            std::cout<<"recv error\n";
        }
        mess[recv_size] = '\0';
        std::string str_message(mess, recv_size);
        std::string str_result = "";
        to_lower(str_message, str_result);
        send(clie_sock, str_result.c_str(), str_result.size(), 0);
    }
    close(clie_sock);
    close(serv_sock);
    return 0;
}

二、基于UDP的服务端/客户端

  • UDP服务端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说不必调用TCP连接过程中的listen()函数和accept()函数,UPD只有 创建套接字 的过程和 数据交换 的过程
  • UPD不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务端和客户端,只能因提供服务的一方称为服务端。
  • 下面使用UDP方式实现 字符串转换 的例子

1、server端代码实现

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
#include <regex>
#include <cctype>
const int BUFFER_SIZE = 128;

void to_lower(const std::string& str_input, std::string& str_output){
    std::regex pattern("^[a-zA-Z]+$");
    bool is_letters = std::regex_match(str_input, pattern);
    if(is_letters){
        str_output.resize(str_input.size());
        std::transform(str_input.begin(), str_input.end(), str_output.begin(), ::tolower);
    }else{
        str_output = "包含其它字符,转换失败!";
    }
    return;
}

int main(void){
    int serv_sock = socket(PF_INET, SOCK_DGRAM, 0);  
    if(serv_sock == -1)
        std::cout<<"socket error\n";

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"bind error\n";

    struct sockaddr_in clie_addr;
    memset(&clie_addr, 0, sizeof(clie_addr));
    socklen_t clie_addr_size = 0;
    int clie_sock = 0;
    while(true){
        char message[BUFFER_SIZE] = {0};
        int mess_len = recvfrom(serv_sock, message, BUFFER_SIZE, 0, (struct sockaddr*)&clie_addr, &clie_addr_size);
        message[mess_len] = '\0';
        std::string str_input(message, mess_len);
        std::string str_output = "";
        to_lower(str_input, str_output);
        sendto(serv_sock, str_output.c_str(), str_output.size(), 0, (struct sockaddr*)&clie_addr, clie_addr_size);
    }
    close(serv_sock);
    return 0;
}

2、client端代码实现

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
const int BUFFER_SIZE = 128;

int main(void){
    int clie_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(clie_sock == -1)
        std::cout<<"socket error\n";

    std::string server_ip = "127.0.0.1";
    struct sockaddr_in serv_addr, from_addr;
    socklen_t from_addr_size = 0;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
    serv_addr.sin_port = htons(8080);

    while(true){
        std::string str_input = "";
        std::cout<<"please input:";
        std::cin>>str_input;
        if(str_input == "q")
            break;

        sendto(clie_sock, str_input.c_str(), str_input.size(), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

        char message[BUFFER_SIZE] = {0};
        int mess_len = recvfrom(clie_sock, message, BUFFER_SIZE, 0, (struct sockaddr*)&from_addr, &from_addr_size);
        message[mess_len] = '\0';
        std::string str_message(message, mess_len);
        std::cout<<"before:"<<str_input<<"\n";
        std::cout<<"later:"<<str_message<<"\n";
    }
    close(clie_sock);
    return 0; 
}

3、sendto() 函数 - 填写地址并传输数据的I/O函数

3.1 函数原型

 	ssize_t sendto(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);
 	// 成功返回传输的字节数,失败返回-1

3.2 参数解析

参数 参数解析
sock 用于传输数据的UDP套接字文件描述符
buff 将要传输数据的地址
nbytes 将要传输数据的长度,以字节为单位
flags 可选项,没有传递0即可
to 存有目标地址信息的sockaddr结构体变量
addrlen to参数的长度

3.3 UDP客户端地址分配

  • 在TCP中客户端是通过connect()函数字符为客户端分配地址的,那UDP中如何分配地址呢?是在 sendto() 函数中进行地址分配的,如果调用sendto()函数时发现没有分配地址信息,则在首次调用时给相应套接字自动分配IP和端口号,IP是主机IP,端口号随机分配。而且此时分配的地址信息回一直保留到程序结束,因此也可以与其它UDP进行数据交换。

4、recvfrom() 函数 - 接收数据

4.1 函数原型

	ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags, sockaddr* from, socklen_t* addrlen);
	// 成功时返回接收的字节数,失败返回-1

4.2 参数解析

参数 参数解析
sock 用于接收数据的UDP套接字文件描述符
buff 接受到数据的缓冲地址
nbytes 可接收的最大字节数,不能超过buff所指的缓冲大小
flags 可选项,没有传递0即可
from 保存发送端的地址信息
addrlen 保存from参数长度的变量的地址值

5、存在数据边界的UDP套接字

  • UDP是具有数据边界的协议,在传输过程中调用I/O函数的次数非常重要。因此,输入函数和输出函数的调用次数应该完全一致,这样才能保证接收全部已发送数据。比如客户端调用了3次sendto()函数,那么服务端也必须调用3次recvfrom()函数才可以接收客户端3次发送的信息。这和TCP不同,TCP客户端调用3次send()函数,那么服务端可以只调用1次recv()函数就可以接收3次发送的信息。

6、创建已连接UDP套接字

  • TCP套接字中需要注册待传输数据的目标IP和端口号,而UDP中则无需注册,因此,通过sendto()函数传输数据的过程大致分为3个阶段:
    (1)、第一阶段:向UDP套接字注册目标IP和端口号
    (2)、第二阶段:传输数据
    (3)、第三阶段:删除UDP套接字中注册的目标地址信息
  • 每次调用sendto函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接connected套接字。显然,UDP套接字默认属于未连接套接字。但UDP套接字在下述情况下显得不太合理: “IP为211.210.147.82的主机82号端口共准备了3个数据,调用3次sendto函数进行传输。”此时需重复3次上述三阶段。因此,要与同一主机进行长时间通信时,将UDP套接字变成已连接套接字会提高效率。上述三个阶段中,第一个和第三个阶段占整个通信过程近1/3的时间,缩短这部分时间将大大提高整体性能。
  • 创建已连接UDP套接字的过程格外简单,只需针对UDP套接字调用connect()函数即可,当然,针对UDP套接字调用connect()函数并不意味着要与对方套接字连接,这只是向UDP套接字注册目标IP和端口号。之后每次调用sendto()函数只需要传输数据,因为已经指定了收发对象,所以此时不仅可以使用sendto()、recvfrom()函数,还可以使用send()和recv()函数。
  • 下面给出已连接UDP套接字client端代码实现,服务端没有变化。

6.1 已连接UDP client端代码实现

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
const int BUFFER_SIZE = 128;

int main(void){
    int clie_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(clie_sock == -1)
        std::cout<<"socket error\n";

    std::string server_ip = "127.0.0.1";
    struct sockaddr_in serv_addr, from_addr;
    socklen_t from_addr_size = 0;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());
    serv_addr.sin_port = htons(8080);
    if(connect(clie_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"connect error\n";
    while(true){
        std::string str_input = "";
        std::cout<<"please input:";
        std::cin>>str_input;
        if(str_input == "q")
            break;
        // sendto(clie_sock, str_input.c_str(), str_input.size(), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
        send(clie_sock, str_input.c_str(), str_input.size(), 0);

        char message[BUFFER_SIZE] = {0};
        // int mess_len = recvfrom(clie_sock, message, BUFFER_SIZE, 0, (struct sockaddr*)&from_addr, &from_addr_size);
        int mess_len = recv(clie_sock, message, BUFFER_SIZE - 1, 0);
        message[mess_len] = '\0';
        std::string str_message(message, mess_len);
        std::cout<<"before:"<<str_input<<"\n";
        std::cout<<"later:"<<str_message<<"\n";
    }
    close(clie_sock);
    return 0; 
}
  • 已连接UDP客户端比未连接UDP客户端只多了connect()函数调用,而且已连接UDP客户端中还可以使用send()和recv()函数进行数据交换,其它部分并没有什么不同。