欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > 【linux】Socket网络编程

【linux】Socket网络编程

2025/2/22 2:17:42 来源:https://blog.csdn.net/fulufulucode/article/details/145440155  浏览:    关键词:【linux】Socket网络编程

目录

0. Socket 编程预备知识

0.1 源IP和目的IP

 0.2 端口号

1. UDP协议

1.1 UDP协议特点

1.2 socket编程接口

1. socket() 函数

2. bind() 函数

 3. sendto() 函数

4. recvfrom() 函数

2. TCP协议

2.1 TCP协议特点

2.2 socket编程接口

1. listen() 函数

2. accept() 函数

3. connect() 函数

4. recv() 函数

5. send() 函数

2.3 TCP连接过程

        2.3.1 如何保证“连接”和“可靠”


0. Socket 编程预备知识

0.1 源IP和目的IP

        IP 在网络中, 用来标识主机的唯一性。

        网络传输的过程,就是将数据从源IP上的对应进程发到目的IP的对应进程,例如使用qq从qq服务器上获得数据。

 0.2 端口号

        端口号(port)是传输层协议的内容
        • 端口号是一个 2 字节 16 位的整数;
        • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
        • IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程;
        • 一个端口号只能被一个进程占用。

        端口号范围划分

        • 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的
端口号都是固定的。
        • 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作
系统从这个范围分配的。

        另外, 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定;
• 进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进
程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这
样做。

        源端口号和目的端口号

        传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号.
就是在描述 "数据是谁发的, 要发给谁"。

        理解 socket
        • 综上, IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的
一个网络进程
        • IP+Port 就能表示互联网中唯一的一个进程
        • 所以, 通信的时候, 本质是两个互联网进程代表人来进行通信, {srcIp,
srcPort, dstIp, dstPort}这样的 4 元组就能标识互联网中唯二的两个进程
        • 所以, 网络通信的本质, 也是进程间通信
        • 我们把 ip+port 叫做套接字 socket

        补充:传输层属于内核,要通过网络协议栈进行通信, 必定调用的是传输层提供的系统调用, 来进行网络通信的。

1. UDP协议

1.1 UDP协议特点

                1.传输层协议。

                2.无连接。

                3.不可靠传输。

                3.面向数据报。

 1.2 网络字节序

        内存中的数据存储有大端和小端之分, 磁盘文件中的数据存储也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

        • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
        • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
        • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
        • TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.
        • 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
        • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

        

于是就有了以下库函数来做数据从主机序列到网络序列的转化:

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

• 这些函数名很好记,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位
短整数。
• 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地
址转换后准备发送。
• 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
• 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

1.2 socket编程接口

        由于UDP是面向数据报的无连接协议,所以数据传输时要使用的接口有:

#include <sys/types.h> 
#include <sys/socket.h>int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
size_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

       接下来逐一介绍: 

1. socket() 函数

socket() 函数用于创建一个套接字(socket),这是网络通信的基本端点。通过套接字,应用程序可以发送和接收数据。

原型

int socket(int domain, int type, int protocol);
  • domain:指定协议族(地址族),常见的值有:

    • AF_INET:IPv4 网络协议。
    • AF_INET6:IPv6 网络协议。
    • AF_UNIX:本地通信协议(仅适用于 UNIX 系统)。
  • type:指定套接字类型,常见的值有:

    • SOCK_STREAM:流套接字,通常用于 TCP(面向连接的协议)。
    • SOCK_DGRAM:数据报套接字,通常用于 UDP(无连接协议)。
  • protocol:指定协议,通常为 0,表示自动选择合适的协议。一般与 type 配合使用,如:

    • 0:自动选择协议。
    • IPPROTO_TCP:用于 TCP。
    • IPPROTO_UDP:用于 UDP。

返回值

  • 成功:返回一个非负整数(套接字描述符)。
  • 失败:返回 -1,并设置 errno

2. bind() 函数

bind() 函数用于将一个套接字与一个本地地址(如 IP 地址和端口号)进行绑定,使得套接字能够接收和发送来自该地址的数据。

原型

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:由 socket() 创建的套接字描述符。
  • addr:指向 sockaddr 结构体的指针,指定本地地址。对于 IPv4,通常使用 sockaddr_in 结构体。
  • addrlen:地址结构体的长度,通常为 sizeof(struct sockaddr_in)

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

 3. sendto() 函数

sendto() 函数用于通过套接字发送数据,通常用于 UDP 等无连接协议。在使用 TCP 协议时,数据通常通过 send() 发送,但对于 UDP 等无连接协议,sendto() 更常用。

原型

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

  • sockfd:由 socket() 创建的套接字描述符。
  • buf:指向要发送的数据的缓冲区。
  • len:要发送的数据的字节数。
  • flags:指定发送操作的标志,通常为 0
  • dest_addr:指向目标地址的指针,通常是 sockaddr_in 类型,包含目标 IP 地址和端口号。
  • addrlen:目标地址的长度,通常为 sizeof(struct sockaddr_in)

返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno

4. recvfrom() 函数

recvfrom() 函数用于接收数据,通常与 sendto() 配合使用,适用于无连接协议(如 UDP)。它能够接收来自任何主机的数据,并获取发送者的地址信息。

原型

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:由 socket() 创建的套接字描述符。
  • buf:接收数据的缓冲区。
  • len:缓冲区的大小,指定最多接收的字节数。
  • flags:指定接收操作的标志,通常为 0
  • src_addr:指向 sockaddr 结构体的指针,用于返回发送者的地址信息。
  • addrlensrc_addr 的长度,调用时传入地址结构的长度,返回时会更新实际长度。

返回值

  • 成功:返回接收到的字节数。
  • 失败:返回 -1,并设置 errno

 其中,struct sockaddr类型是什么,用下面的示例图来理解:

sockaddr其实是c语言实现多态的一种方式,它使得sockaddr_in和sockaddr_un两种结构类型可以用同一个bind函数统一处理。

如果要使用bind函数,我们首先要创建一个sockaddr_in,将端口号ip地址等信息填充进去,然后bind函数就可以把进程和对应的套接字绑定起来,然后就可以从该套接字中收取信息。sendto函数中的sockaddr是为了指明目的ip+端口,而recvfrom中的是为了获取源ip+端口,方便后续回传信息等。

那么设置sockaddr的操作如何完成呢,且看完整的使用示例:

int main()
{int _sockfd = ::socket(AF_INET,SOCK_DGRAM,0);if(_sockfd<0){LOG(ERROR)<<"创建套接字失败";return 1;}LOG(INFO)<<"创建套接字成功,socket is:"<<_sockfd;//填充网络信息struct sockaddr_in local;//首先将sockaddr_in结构体初始化bzero(&local,sizeof(local));//对应结构体成员赋值local.sin_family = AF_INET;//协议类型local.sin_port = ::htons(8080);//端口local.sin_addr.s_addr = INADDR_ANY;//ip地址,INADDR_ANY表示接收任意ip连接//设置信息int n = ::bind(_sockfd,(sockaddr*)&local,sizeof(local));if(n<0){LOG(ERROR)<<"bind失败";return 2;}LOG(INFO)<<"bind成功";while(true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(sockaddr*)&peer,&len);if(n > 0){buffer[n] = 0;std::string echo_string = "sever# ";echo_string += buffer;LOG(INFO)<<"收到客户端消息:"<<echo_string;sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(sockaddr*)&peer,len);}}return 0;
}

以上实现的是服务端接收并回传客户端发送的消息,在看看对应的客户端代码:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <cstring>
#include <string>using namespace LogModule;int main()
{int sockfd = ::socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){LOG(ERROR)<<"socket创建失败";}LOG(INFO)<<"socket创建成功";//填充服务器信息struct sockaddr_in sever;sever.sin_family = AF_INET;sever.sin_port = ::htons(8080);sever.sin_addr.s_addr = inet_addr("127.0.0.1");//设置目标服务器的ip//127.0.0.1是一个特殊的ip//意思是本地ipwhile(true){//发送消息std::cout<<"please enter:"<<std::endl;std::string meassage;std::getline(std::cin,meassage);::sendto(sockfd,meassage.c_str(),meassage.size(),0,(sockaddr*)&sever,sizeof(sever));//接收消息struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];int n = ::recvfrom(sockfd,buffer,sizeof(buffer),0,(sockaddr*)&temp,&len);if(n>0){buffer[n] = 0;std::cout<<buffer<<std::endl;}}return 0;
}

2. TCP协议

2.1 TCP协议特点

        1. 传输层协议
        2. 有连接
        3. 可靠传输
        4. 面向字节流

区别于UDP协议,TCP传输更加可靠(丢失数据会重传),并且是有连接的(先建立连接再通信),并且是面向字节流传输数据(可以复用write,read函数发收数据)。

2.2 socket编程接口

        下面仅介绍TCP不同于UDP的接口。

1. listen() 函数

listen() 函数用于在服务器端套接字上进行监听,等待客户端的连接请求。它在服务器端调用,告知操作系统该套接字准备接收连接请求。

原型

int listen(int sockfd, int backlog);
  • sockfd:由 socket() 创建的套接字描述符。
  • backlog:指定等待连接的队列的最大长度,通常设置为一个合理的值,如 5。如果队列满了,新的连接请求可能会被拒绝。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

2. accept() 函数

accept() 函数用于接受一个客户端的连接请求,它在服务器端调用。成功建立连接后,返回一个新的套接字描述符,该套接字用于与客户端通信。

原型

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:由 socket() 和 listen() 创建的监听套接字描述符。
  • addr:指向 sockaddr 结构体的指针,接受客户端的地址信息(如 IP 地址和端口号)。对于 IPv4,通常使用 sockaddr_in 结构体。
  • addrlenaddr 结构体的长度。调用时传入地址结构的大小,返回时会更新实际长度。

返回值

  • 成功:返回一个新的套接字描述符,用于与客户端进行通信。
  • 失败:返回 -1,并设置 errno

3. connect() 函数

connect() 函数用于客户端连接到服务器端,客户端通过调用 connect() 与服务器端的指定地址建立连接。

原型

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:由 socket() 创建的套接字描述符。
  • addr:指向 sockaddr 结构体的指针,指定目标服务器的地址。
  • addrlenaddr 结构体的长度。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

4. recv() 函数

recv() 函数用于从连接的套接字中接收数据,通常在客户端和服务器端之间进行通信时使用。

原型

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:由 socket()connect() 或 accept() 创建的套接字描述符。
  • buf:指向缓冲区的指针,用于存放接收到的数据。
  • len:缓冲区的大小,指定最多接收的字节数。
  • flags:指定接收操作的标志,通常为 0

返回值

  • 成功:返回实际接收到的字节数。
  • 失败:返回 -1,并设置 errno
  • 如果连接被关闭:返回 0

5. send() 函数

send() 函数用于通过已连接的套接字发送数据,通常在客户端和服务器端之间进行数据传输时使用。

原型

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:由 socket()connect() 或 accept() 创建的套接字描述符。
  • buf:指向要发送的数据的缓冲区。
  • len:要发送的数据的字节数。
  • flags:指定发送操作的标志,通常为 0

返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno

使用示例(同样是实现回传客户端信息):

服务端代码:

int main()
{int listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (listensockfd < 0){LOG(ERROR) << "创建套接字失败";return 1;}LOG(INFO) << "创建套接字成功,listensockfd is:" << listensockfd;// 填充网络信息struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = ::htons(8080);local.sin_addr.s_addr = INADDR_ANY;// 设置信息int n = ::bind(listensockfd, (sockaddr *)&local, sizeof(local));if (n < 0){LOG(ERROR) << "bind失败";return 2;}LOG(INFO) << "bind成功";n = ::listen(listensockfd, BACKLOG);if (n < 0){LOG(ERROR) << "listen fail";return 3;}struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(listensockfd, (sockaddr *)&peer, &len);if (sockfd < 0){LOG(ERROR) << "accept error";return 4;}LOG(INFO) << "accept success, sockfd is:" << sockfd;char buffer[1024];while (true){ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;std::string echo_string = "sever# ";echo_string += buffer;LOG(INFO) << "收到客户端消息:" << echo_string;send(sockfd, echo_string.c_str(), echo_string.size(), 0);}}return 0;
}

客户端代码:

int main()
{int sockfd = ::socket(AF_INET,SOCK_STREAM,0);if(sockfd < 0){LOG(ERROR)<<"socket创建失败";}LOG(INFO)<<"socket创建成功";//填充服务器信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = ::htons(8080);server.sin_addr.s_addr = inet_addr("127.0.0.1");int n = ::connect(sockfd,(sockaddr*)&server,sizeof(server));while(true){//发送消息std::cout<<"please enter:"<<std::endl;std::string meassage;std::getline(std::cin,meassage);//::sendto(sockfd,meassage.c_str(),meassage.size(),0,(sockaddr*)&server,sizeof(server));::send(sockfd,meassage.c_str(),meassage.size(),0);//接收消息struct sockaddr_in temp;socklen_t len = sizeof(temp);char buffer[1024];//int n = ::recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(sockaddr*)&temp,&len);int n = ::recv(sockfd,buffer,sizeof(buffer)-1,0);if(n>0){buffer[n] = 0;std::cout<<buffer<<std::endl;}}return 0;
}

以上就是TCP协议的基本使用方式,接下来详细介绍一下TCP实现“连接”,以及“可靠”。 

2.3 TCP连接过程

        2.3.1 如何保证“连接”和“可靠”

在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接

以上是TCP三次握手建立连接,四次挥手断开连接的过程,其中SYN,ACK等是 TCP 协议段格式中的标志位。

TCP协议段格式如下:

• 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
• 32 位序号/32 位确认号: 后面详细讲;
• 4 位 TCP 报头长度: 表示该 TCP 头部有多少个 32 位 bit(有多少个 4 字节); 所以
TCP 头部最大长度是 15 * 4 = 60

6 位标志位:





URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段

SYN: 请求建立连接; 我们把携带 SYN 标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段

• 16 位窗口大小:收发信息的缓冲区大小,会根据网络状况调整。

• 16 位校验和: 发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此
处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分.
• 16 位紧急指针: 标识哪部分数据是紧急数据;

其中序列号和确认序列号为:

TCP 将每个字节的数据都进行了编号. 即为序列号.

每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下
一次你从哪里开始发,发送者会通过对吧序列号和确认序列号来进行补发数据和超时重传。

确认应答:

超时重传:
 

• 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;
• 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发;

 

当然,也可能主机A并没有收到ACK请求,而导致超时重传

此时主机B会收到重复的信息,但是主机B可以通过数据的序列号来去重。 

以上就是TCP协议连接的完整建立过程,以及通信过程,也是它为何能保证“连接”和“可靠”的原因。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词