【无标题】


title: linux网络编程
date: 2024-04-07 12:00:00
categories:

  • linux
  • socket
    tags: 网络通信基础

socket

想象一下,客户端和服务端的通信就像是在两个人之间传递秘密信息的过程。

服务端就像是一个有家的人。首先,他需要建立一个家(创建套接字),然后告诉大家他的家在哪里(绑定IP地址和端口),打开门等待访客(监听连接)。当有人敲门(客户端发起连接)时,他会开门(接受连接),然后他们可以在屋子里自由地交谈(数据交换)。访问结束后,客人离开,他会关门(关闭套接字)。

客户端就像是一个想要拜访朋友的人。他知道朋友的地址(服务端的IP地址和端口),所以他出发去朋友家(创建套接字),敲门(发起连接)。朋友开门后,他们开始聊天(数据交换)。聊完后,他告别离开(关闭套接字)。

整个过程就像是朋友之间的一次访问,通过敲门、交谈和告别来完成一次愉快的交流。


客户端:

创建流式socket —socket()

int sockfd = socket( AF_INET , SOCK_STREAM , 0 );

函数原型:int socket(int domain,int type,int protocol);

参数:

  • domain 通讯协议族:通常为Ipv4即PF_INET,

    • PF_INET IPv4互联网协议族.
    • PF_INET6 IPv6互联网协议族。
    • PF_LOCAL 本地通信的协议族。
    • PF_PACKET 内核底层的协议族。
    • PF_IPX IPX Novell协议族。
  • type 数据传输方式:

    • SOCK_STREAM 面向连接的socket:
      • 1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。
    • SOCK_DGRAM 无连接的socket:
      • 1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。
  • protocol协议:分别与第二个参数对应的协议为IPPROTO_TCP和IPPRPTP_UDP

    (或填0自动识别)

成功返回一个有效的socketid,失败返回-1,errno被设置。

向服务器发起连接请求 —connect()

准备连接所需IP以及Port
1.gethostbyname

获取字符传并转换为网络字节序IP

根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。

struct hostent *gethostbyname(const char *name);

struct hostent {

char *h_name; // 主机名。

char **h_aliases; // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。

short h_addrtype; // 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。

short h_length; // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。

char **h_addr_list; // 主机的ip地址,以网络字节序存储。

};

#define h_addr h_addr_list[0] // for backward compatibility.

转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员中。

memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

2.sockaddr与sockaddr_in

使用sockaddr与sockaddr_in存储IP和Port,一般都需要定义为sockaddr_in在最后连接时再转换为sockaddr

>- 存放协议族、端口和地址信息,客户端和connect()函数和服务端的bind()函数需要这个结构体。
>
>struct sockaddr {
>
>  unsigned short sa_family;      // 协议族,与socket()函数的第一个参数相同,填AF_INET。
>
>  unsigned char sa_data[14];    // 14字节的端口和地址。
>
>};
>
>- sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。
>
>  struct sockaddr_in {  
>
>    unsigned short sin_family;     // 协议族,与socket()函数的第一个参数相同,填AF_INET。
>
>    unsigned short sin_port;        // 16位端口号,大端序。用htons(整数的端口)转换。
>
>    struct in_addr sin_addr;          // IP地址的结构体。192.168.101.138
>
>    unsigned char sin_zero[8];     // 未使用,为了保持与struct sockaddr一样的长度而添加。
>
>  };
>
>  struct in_addr {                      // IP地址的结构体。
>
>    unsigned int s_addr;        // 32位的IP地址,大端序。
>
>  };
3.addinfo

这里有一个函数getaddrinfo,可以直接将之前的两步简化

getaddrinfo(m_ip.c_str(), std::to_string(m_port).c_str(), &hints, &res);

  • m_ip.c_str():这是一个指向以 null 结尾的字符串的指针,包含主机名或者 IP 地址。在这个例子中,m_ip 是一个 std::string 对象,它存储了服务器的 IP 地址或主机名。使用 c_str() 方法将 std::string 转换为 C 风格的字符串。
  • std::to_string(m_port).c_str():这是一个指向以 null 结尾的字符串的指针,包含服务名或端口号。m_port 是一个整数,表示端口号。使用 std::to_string 将整数转换为 std::string,然后使用 c_str() 方法将其转换为 C 风格的字符串。
  • &hints:这是一个指向 addrinfo 结构的指针,该结构提供了关于期望返回的地址类型的提示。在这个结构中,可以设置地址族(如 AF_INET),套接字类型(如 SOCK_STREAM),协议类型(如 IPPROTO_TCP),以及其他可能的选项。
  • &res:这是一个指向 addrinfo 结构指针的指针。函数成功执行后,res 将指向一个 addrinfo 结构链表,其中每个结构包含一个可用于 bind(2)connect(2) 调用的网络地址。

这个方法主要做了几件事:

getaddrinfo

  1.解析:将传入的 IP 地址或主机名(m_ip.c_str())和服务名或端口号(std::to_string(m_port).c_str())解析为具体的网络地址
  2.填充:根据 hints 结构提供的参数(如协议族、套接字类型、协议类型等),getaddrinfo 会搜索符合条件的网络地址。
  3.返回:它会分配一个或多个 addrinfo 结构,并将它们链接成一个链表。每个 addrinfo 结构包含了可以用于创建套接字和建立连接的地址信息。
  4.赋值:函数成功后,res 指针会指向这个链表的头部,你可以遍历这个链表,使用其中的信息来创建套接字或进行其他网络操作

将C风格ip与port字符串以及包含协议族,套接字类型,协议类型的hints这三个参数一起用来解析,完成后分配addrinfo结构体的链表并将表头地址赋给res

//addrinfo 结构体,它包含了以下信息:
ai_family	:地址族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
ai_socktype	:套接字类型,如 SOCK_STREAM(流式套接字)或 SOCK_DGRAM(数据报套接字)。
ai_protocol	:协议类型,如 IPPROTO_TCP(TCP 协议)或 IPPROTO_UDP(UDP 协议)。
ai_addrlen	:地址长度,表示 ai_addr 指向的地址的长度。
ai_addr		:指向一个 sockaddr 结构的指针,该结构包含具体的网络地址。
ai_canonname:如果请求规范名,这是主机的规范名。
ai_next		:指向链表中下一个 addrinfo 结构的指针。

最终看一下两个版本的代码

gehostbynamey获取port+sockaddr保存信息:

  struct hostent* h; 
  if ( (h = gethostbyname(argv[1])) == 0 )
  { 
    cout << "gethostbyname failed.\n" << endl; 
    close(sockfd);
    return -1;
  }
  struct sockaddr_in servaddr;              		
  memset(&servaddr,0,sizeof(servaddr));     		
  servaddr.sin_family = AF_INET;            	
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
  servaddr.sin_port = htons(atoi(argv[2]));         

addinfo:

    struct addrinfo hints,*res = NULL;
    memset(&hints,0,sizeof(hints));
    hints.ai_family = AF_INET;          //ipv4
    hints.ai_socktype = SOCK_STREAM;    //TCP流
    hints.ai_protocol = IPPROTO_TCP;
    std::string ip = argv[1];
    int port = atoi(argv[2]);
	//解析服务器地址和端口
    int status = getaddrinfo(ip.c_str(),(std::to_string(port)).c_str(),&hints,&res);
    if (status!=0)
    {
        std::cerr<<"getaddrinfo error"<<gai_strerror(status)<<std::endl;
        return -1;
    }
-------------------
	//创建socket
	m_connfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	//int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(m_connfd == INVALID_SOCKET)
	{/*...*/}
	//连接到服务器
	if (connect(m_connfd, res->ai_addr, (int)res->ai_addrlen) != 0)
	{/*...*/}
	freeaddrinfo(res);
	return true;

整洁美观。

实际上addrinfo和sockaddr之间并不是替代关系,在某种意义上,addrinfo 可以看作是 sockaddr 的替代者,因为它提供了更完整的信息集合。但实际上,它们是互补的:addrinfo 用于获取和存储地址信息,而 sockaddr 用于在socket API中表示地址

connect

连接:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:客户端的套接字文件描述符。
  • addr:指向struct sockaddr的指针,包含了目标服务端的地址信息,包括IP地址和端口号。
  • addrlenaddr结构体的大小。

返回值:

  • 成功时返回0。
  • 失败时返回-1,并设置errno以指示错误类型。

功能描述:

  • 当客户端调用connect()函数时,它会发起对服务端的连接请求。
  • 这个过程包括TCP协议的三次握手,确保客户端和服务端之间建立稳定的连接。
  • 三次握手成功后,客户端和服务端之间的套接字就可以用于发送和接收数据。

使用connect()函数时,客户端通常会阻塞,直到连接建立成功或者发生错误。如果连接成功,客户端就可以通过send()recv()函数与服务端进行数据交换。

发送/接收数据 —send()/recv()

send() 函数:
  • 用于向TCP连接的另一端发送数据。

  • 函数原型:

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

  • 参数:

    • sockfd:连接的套接字文件描述符。
    • buf:指向要发送数据的缓冲区。
    • len:要发送的数据长度。
    • flags:提供额外的信息来控制发送行为,一般设置为0。
  • 返回值:

    • 成功时返回实际发送的字节数。
    • 失败时返回-1,并设置errno以指示错误类型。
recv() 函数:
  • 用于接收来自套接字缓冲区的数据。

  • 函数原型:

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

  • 参数:

    • sockfd:连接的套接字文件描述符。
    • buf:指向用于接收数据的缓冲区。
    • len:缓冲区长度。
    • flags:提供额外的信息来控制接收行为,一般设置为0。
  • 返回值:

    • 成功时返回实际读到的字节数。
    • 如果在等待协议接收数据时网络中断了,那么它返回0。
    • 失败时返回-1,并设置errno以指示错误类型。

在使用send()recv()函数时,需要注意的是,它们都可能因为网络延迟或其他原因而阻塞。在阻塞模式下,如果发送/接收缓冲区不可用,这些函数会等待直到缓冲区变得可用。在非阻塞模式下,如果操作会导致阻塞,这些函数会立即返回EAGAINEWOULDBLOCK错误。此外,如果send()在发送数据时遇到对端关闭连接的情况,会收到SIGPIPE信号。

关闭连接,释放资源 —close()

close(sockfd);

在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换,只有向sockaddr_in结体成员变量填充数据时,才需要考虑字节序的问题。

/*
 * 此程序用于演示socket的客户端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
 
int main(int argc,char* argv[])
{
  if (argc!=3)
  {
    cout << "Using:./demo1 服务端的IP 服务端的端口\nExample:./demo1 IP Port\n\n"; 
    return -1;
  }
--------------------------------------------------------------------------------------------------
  // 第1步:创建客户端的socket。  
  //创建了一个ipv4的socket,得到了这个socket的描述符
  int sockfd = socket(AF_INET,SOCK_STREAM,0);
  if (sockfd==-1)//所有网络编程失败返回-1     
  {
    perror("socket"); return -1;
  }
 -----------------
  // 第2步:向服务器发起连接请求。 
 ------
  //a.获取并保存IP+Prot。发起请求需要从main参数中取得服务器的IP与端口
 ---
     //①.获取IP并转换网络字节序
  struct hostent* h;    // 用于存放服务端IP的结构体。
  if ( (h = gethostbyname(argv[1])) == 0 )  // 该函数根据域名/主机名/字符串IP获取大端序IP。
  { 
    cout << "gethostbyname failed.\n" << endl; 
    close(sockfd);//无法获取则关闭socket并推出程序
    return -1;
  }
---
  //②.将获取到的IP存入一个socket"专用"结构体(sockaddr/sockaddr_in)
  /*sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。*/
  struct sockaddr_in servaddr;              		// 用于存放服务端IP和端口的结构体。
  memset(&servaddr,0,sizeof(servaddr));     		//初始化置为全零
  servaddr.sin_family = AF_INET;            		// sin_family设置结构体第一个参数-协议族-Ipv4
  memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);	 // sin_addr指定服务端的IP地址。将gethostbyname转换得到的IP存入
  servaddr.sin_port = htons(atoi(argv[2]));          // 指定服务端的通信端口。unsigned short-unit16
------
//b.准备完毕,发起连接请求
  if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0)  // 向服务端发起连接清求。
  { 
    perror("connect"); 
    close(sockfd); 
    return -1; 
  }
-----------------
  // 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
  char buffer[1024];
  for (int i=0;i<4;ii++)  // 循环3次,将与服务端进行三次通讯。
  {
    int iret;
    memset(buffer,0,sizeof(buffer));
    sprintf(buffer,"这是第%d个,编号%03d。",i+1,i+1);  // 生成请求报文内容。
    // 向服务端发送请求报文。
    if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
    { 
      perror("send"); break; 
    }
    cout << "发送:" << buffer << endl;

    memset(buffer,0,sizeof(buffer));
    // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
    if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
    {
       cout << "iret=" << iret << endl; break;
    }
    cout << "接收:" << buffer << endl;

    sleep(1);
  }
 
  // 第4步:关闭socket,释放资源。
  close(sockfd);
}

服务端

相似的部分不再复述

  • 第二步绑定中(line11)的htonl

为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。

C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

uint16_t h to n s(uint16_t hostshort); // uint16_t 2字节的整数 unsigned short

uint32_t htonl(uint32_t hostlong); // uint32_t 4字节的整数 unsigned int

uint16_t ntohs(uint16_t netshort);

uint32_t n to h l(uint32_t netlong);

h host(主机);

to 转换;

n network(网络);

s short(2字节,16位的整数);

l long(4字节,32位的整数);

  • bind绑定服务端的IP和端口到listenfd这个socket上(为socket分配ip和port)。

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

    参数说明:

    • sockfd:套接字文件描述符,即代表socket()函数创建的套接字文件。
    • addr:指向一个struct sockaddr类型的结构体变量,此结构体成员用于设置要绑定的IP和端口。
    • addrlen:第二个参数所指向的结构体变量的大小。

    返回值:

    • 成功时返回0。
    • 失败时返回-1,并设置errno以指示错误类型。
  • listen设置当前状态为监听

    int listen(int sockfd, int backlog);
    

    参数说明:

    • sockfd:指向已经绑定到一个本地地址的套接字的文件描述符。
    • backlog:定义了套接字可以排队等待的挂起连接的最大数量。

    返回值:

    • 成功时返回0。
    • 失败时返回-1,并设置errno以指示错误类型。

    功能描述:

  • accept受理请求

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    

    参数说明:

    • sockfd:监听套接字的文件描述符,它是之前通过socket()创建并通过bind()listen()设置为监听状态的套接字。
    • addr:(可选)指向struct sockaddr结构的指针,用于接收连接的客户端的地址信息。
    • addrlen:(可选)指向socklen_t类型的变量的指针,表示addr的长度。在调用前,应该设置为addr结构的大小;调用后,会被设置为实际接收到的地址的大小。

    返回值:

    • 成功时返回一个新的套接字文件描述符,用于与客户端进行通信。
    • 失败时返回-1,并设置errno以指示错误类型。

    功能描述:

    • 当服务器端的套接字处于监听状态时,accept()函数会检查是否有已完成的连接请求在队列中。
    • 如果有,accept()会创建一个新的套接字,并将其与发起连接的客户端关联起来。这个新的套接字不同于监听套接字,它专门用于与该客户端的通信。
    • 如果addraddrlen参数不为NULL,accept()还会将客户端的地址信息写入addr所指向的结构,并更新addrlen所指的值为实际地址的长度。
    • 如果在调用accept()时没有已完成的连接请求,那么根据套接字的阻塞状态,accept()可能会阻塞等待,直到有连接请求完成或者发生错误。

    服务端通过accept利用绑定好的且处于监听状态的监听套接字与客户端的请求匹配,成功后返回一个新的套接字用于回复客户端

    回复与发送的内容都在buffer中进行

/*
 * 此程序用于演示socket通信的服务端
*/
//第一步相同
// 第1步:创建服务端的socket。 
  int listenfd = socket(AF_INET,SOCK_STREAM,0);
  if (listenfd==-1) 
  { 
    perror("socket"); 
    return -1; 
  }
-------------------------------
  /*第2步:把服务端用于通信的IP和端口绑定到socket上*/
  //a.准备ip与端口
  struct sockaddr_in servaddr;          // 用于存放服务端IP和端口的数据结构。
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;                // 指定协议。与客户端相同
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯,服务端可以与多个客户端建立连接。
  servaddr.sin_port = htons(atoi(argv[1]));     // 指定通信端口,端口需要与客户端相同
 
 //b.绑定服务端的IP和端口到listenfd这个socket上(为socket分配ip和port)。
  if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
  { 
    perror("bind"); 
    close(listenfd); 
    return -1; 
  }
 
  // 第3步:把socket设置为可连接(监听)的状态。
  if (listen(listenfd,5) != 0 ) 
  { 
    perror("listen"); 
    close(listenfd); 
    return -1; 
  }
 
  // 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
//accept利用监听套接字连接客户端
  int clientfd=accept(listenfd,0,0);
  if (clientfd==-1)
  {
    	perror("accept"); c
        lose(listenfd); 
     	 return -1; 
  }

  cout << "客户端已连接。\n";
 
  // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
  char buffer[1024];
  while (true)
  {
    int iret;
    memset(buffer,0,sizeof(buffer));
    // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
    // 如果客户端已断开连接,recv()函数将返回0。
    if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) 
    {
       cout << "iret=" << iret << endl;  break;   
    }
    cout << "接收:" << buffer << endl;
 
    strcpy(buffer,"ok");  // 生成回应报文内容。
    // 向客户端发送回应报文。
    if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) 
    { 
      perror("send"); break; 
    }
    cout << "发送:" << buffer << endl;
  }
 
  // 第6步:关闭socket,释放资源。
  close(listenfd);   // 关闭服务端用于监听的socket。
  close(clientfd);   // 关闭客户端连上来的socket。
}

  • 28
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值