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)传输的效率更高。
- SOCK_STREAM 面向连接的socket:
-
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地址和端口号。addrlen
:addr
结构体的大小。
返回值:
- 成功时返回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()
函数时,需要注意的是,它们都可能因为网络延迟或其他原因而阻塞。在阻塞模式下,如果发送/接收缓冲区不可用,这些函数会等待直到缓冲区变得可用。在非阻塞模式下,如果操作会导致阻塞,这些函数会立即返回EAGAIN
或EWOULDBLOCK
错误。此外,如果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
以指示错误类型。
功能描述:
- 当调用
listen()
函数时,内核会为相应的套接字启动一个监听队列,用于存放等待处理的客户端连接请求。这个队列的长度由backlog
参数指定。 backlog
参数告诉系统该套接字在开始拒绝新的连接请求之前,可以排队的未完成连接请求的最大数量。这个值通常由系统管理员根据系统资源和应用程序的需求进行配置。- 在TCP三次握手过程中,当第一次握手(SYN)包到达服务器时,服务器会将这个连接放入一个半连接队列中。如果队列满了,新的连接请求可能会被忽略。
backlog
参数就是用来设置这个半连接队列的长度的
-
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()
会创建一个新的套接字,并将其与发起连接的客户端关联起来。这个新的套接字不同于监听套接字,它专门用于与该客户端的通信。 - 如果
addr
和addrlen
参数不为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。
}