首页 > 系统相关 >Linux网络:TCP & UDP socket

Linux网络:TCP & UDP socket

时间:2024-08-27 21:25:58浏览次数:7  
标签:sockaddr UDP socket sock struct TCP 接字 sin addr

Linux网络:TCP & UDP socket


本博客讲解 Linux 下的 TCP 和 UDP 套接字编程。无论是创建套接字、绑定地址,还是发送和接收数据,都会详细讲解。希望这篇博客能帮你更轻松地理解这些概念,并在实践中得心应手。

socket 套接字

套接字是网络通信的基础,它提供了一种标准化的方式,使得程序能够通过网络发送和接收数据。

套接字的种类非常多样,比如:

  • unix socket域套接字:用于本地通信
  • inet socket网络套接字:用于网络通信
  • raw socket原始套接字:用于网络管理

sockaddr

sockaddr 是一个在网络编程中用于表示“套接字地址”的通用结构体。它的作用是存储网络地址信息,供套接字函数使用,此时套接字函数就知道要对哪一台主机进行网络操作。sockaddr包含在头文件<arpa/inet.h>中。

sockaddr 结构体不能直接存储 IPv4 或 IPv6 的地址信息,在实际使用中,通常会用到它的具体子类型,如 sockaddr_in(用于 IPv4)和 sockaddr_in6(用于 IPv6),sockaddr_un(用于域套接)。

如图:

在这里插入图片描述

为了管理多种套接字,所有套接字的头部都是一个16位的地址类型,用于辨别这个结构体表示哪一个套接字。当操作sockaddr的时候,读取前16位就知道这个sockaddr具体是哪一种套接字。随后再进行类型转化,变成对应套接字类型的结构体,此时就能对具体的套接字做操作了。

sockaddr的定义如下:

struct sockaddr {
    unsigned short sa_family;   
    char sa_data[14];           
};
  • sa_family:表示协议族,例如 IPv4 使用 AF_INET,IPv6 使用 AF_INET6,域套接使用AF_UNIX

其中最常用的就是AF_INET进行IPv4通信。其对应的具体结构体为struct sockaddr_in,定义如下:

struct sockaddr_in {
  sa_family_t		sin_family;
  __be16		sin_port;
  struct in_addr	sin_addr;

  unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) -
			sizeof(unsigned short int) - sizeof(struct in_addr)];
};
  • sin_family:前16位,表示套接字类型
  • sin_port:表示端口号
  • sin_addr:表示IPv4地址

此处有一个小细节,sin_addr的类型是struct in_addr,按理来说IPv4的地址占32位,用一个int类型即可存储,这里的结构体又是啥?

其实是Linux对其进行了额外的一层封装:

struct in_addr {
	__be32	s_addr;
};

此处的__be32就是一个32位的整型,也就是说存储地址的时候,要用sockaddr_in.sin_addr.s_addr,此处嵌套了两层结构体

基于IP地址和端口号,此时就可以定位到全世界的一个主机上的一个具体进程,此时就可以进行后续的网络通信了!


网络字节序

在不同主机内存中,字节数据分为大端字节序和小端字节序,假设一个大端主机和一个小端主机进行通信,此时就会发生错误,因为两别解析数据的方式不同,于是网络字节序出现了。

TCP/IP 规定,大端字节序为网络字节序,在网络中通信必须使用网络字节序

也就是说,如果当前主机是大端主机,那么收发数据时不做处理。如果是小端主机,那么收发数据时要把数据转化为小端字节序。

在此处讲解这个问题,就是因为在填写sockaddr_in内部的IP和端口号时,内部数据的字节序要使用网络字节序

假设我们现在要往sockaddr_in 内部填入端口号22:

struct sockaddr_in sock;
sock.sin_port = 22;

以上就是一个错误示例,因为不清楚代码的运行环境是大端还是小端,此时存入的数据22就有可能不是网络字节序,所以要先将22转为网络字节序,Linux为此提供了专门的接口。

以下接口用于序列转化,需要头文件<arpa/inet.,h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

上面接口的用法非常好记,比如htonl拆解为host to net long,也就是将long类型从主机的字节序转化为网络字节序。同理htns就是将short类型从主机的字节序转化为网络字节序。

反过来,ntohl就是net to host long,将long类型从网络字节序转化为主机的字节序。

如果主机是大端,那么这些函数对数据不做任何处理。如果主机是小端,那么就会进行大小端的转化。

由于sin_port 的类型是int16,所以在写入端口前要用htons进行转化:

struct sockaddr_in sock;
sock.sin_port = htons(22);

IP地址转换

如果想要给一个sockaddr_in结构体填入数据,那么第一个问题就是IP地址的格式问题。

IP地址有两种基本格式,4字节序列,以及点分十进制,如果拿到的IP地址格式与自己所需的类型不符,此时就要考虑两种格式之间转化的问题了。但是不必担心,这个问题也有对应的接口解决。

以下函数需要头文件:<sys/socket.h><netinet/in.h><arpa/inet.h>

inet_addr用于将点分十进制转化为四字节序列:

in_addr_t inet_addr(const char *cp);
  • cp:指向点分十进制IP地址字符串的指针

如果转化错误,返回INADDR_NONE,本质上是数字-1

示例:

struct sockaddr_in sock;
sock.sin_addr.s_addr = inet_addr("127.0.0.1");

此处要注意,实际存储IP序列的是.sin_addr.s_addr,这里有两层结构体嵌套。

那么有人就有疑问了,都说了存入sockaddr_in 中的数据必须是网络字节序,此处将点分十进制转化为四字节序列后,是不是要再转化为网络字节序?

比如这样:

struct sockaddr_in sock;
sock.sin_addr.s_addr = htons(inet_addr("127.0.0.1"));

其实不用,inet_addr会完成两个任务:

  1. 将点分十进制转化为四字节序列
  2. 将四字节序列转化为网络字节序

也就是说inet_addr内部已经顺带完成了转化网络字节序的工作

inet_ntoa用于将四字节序列转化为点分十进制:

char *inet_ntoa(struct in_addr in);

bzero

目前初始化一个sockaddr_in的代码如下:

struct sockaddr_in sock;
sock.sin_addr.s_addr = inet_addr("127.0.0.1");
sock.sin_port = htons(22);

一个要点是IP地址格式的转化,另一个要点是网络字节序。

但是这样还不完整,还有一个成员sin_family没有初始化,对于IPv4通信,此处填入AF_INET,这个在之前已经说过。

struct sockaddr_in sock;
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = inet_addr("127.0.0.1");
sock.sin_port = htons(22);

目前为止已经初始化了一个比较完整的sockaddr_in了。但是创建结构体时,分配到的内存原先有可能存储了其他数据,因为有填充部分我们不初始化,为了保证之前的数据不会影响,还要把整个结构体的内存全部置为0

bzero用于初始化一段内存为0,需要头文件<strings.h>,定义如下:

void bzero(void* s, size_t n);
  • s:要初始化内存的地址
  • n:要初始化的字节数

在初始化整个sockaddr_in 之前,先用bzero将内存清零:

struct sockaddr_in sock;
bzero(&sock, sizeof(sock));

现在我们就有一个比较完整的sockaddr_in 初始化过程了:

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

上述所有内容,都只是在初始化一个套接字所需的地址,还没有真正的创建套接字,接下来就了解操作真正的套接字的接口。


UDP socket

socket

socket函数用于创建一个新的套接字,需要头文件<sys/types.h><sys/socket.h>,函数原型如下:

int socket(int domain, int type, int protocol);

参数:

  • domain:指定协议族
  • type:指定套接字类型
  • protocol:通常设为 0,表示使用默认协议

返回值:

  • 成功:返回新创建的套接字的文件描述符
  • 失败:返回 -1

如果要创建UDP套接字,那么参数应该填为:

int fd = socket(AF_INET, SOCK_DGRAM, 0);
  • AF_INET:表示IPv4
  • SOCK_DGRAM:表示UDP套接字,DGRAMdatagram缩写,即数据报

socket的返回值是一个整型,本质是一个文件描述符,Linux一切皆文件,后续对网络的操作就是对这个文件的操作。比如向网络中发送消息,其实就是向文件中写入数据


bind

当创建完套接字后,这个套接字还没有指定和哪一个主机通信,此时就需要IP地址和端口号,之前讲的sockaddr_in就派上用场了!

bind函数用于给套接字绑定IP地址和端口号,指定和哪一台主机通信,需要头文件<sys/types.h><sys/socket.h>,函数原型如下:

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

参数:

  • sockfd:套接字对应的文件描述符
  • addr:指向sockaddr的指针
  • addrlensockaddr的真实类型的长度

返回值:

  • 成功:返回0
  • 失败:返回-1,并设置错误码

之前讲解的sockaddr_insockaddr的一种,此处注意传入的是struct sockaddr *,也就是说sockaddr_in类型的变量传入的时候要进行类型转化。

由于不知道struct sockaddr *具体指向哪一种套接字地址,所以第三个参数要传入真实类型结构体大小,防止越界访问。

示例:

// 1.创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// 2.初始化套接字要通信的目标主机地址
struct sockaddr_in sock;
bzero(&sock, sizeof(sock));
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = inet_addr("1.2.3.4");
sock.sin_port = htons(8888);

// 3.绑定地址到套接字
bind(sockfd, (struct sockaddr*)&sock, sizeof(sock));

比如以上示例中,绑定的地址为1.2.3.4,端口为8888,这表明只有1.2.3.4可以通过端口8888和这个套接字与本主机进行通信

此处不代表1.2.3.4以外的地址不能与当前主机通信了,只是对于被绑定套接字而言。就好像一个学校有好几个大门,其中某个门叫做“校长专用通道”,那么只有校长可以通过这个门进入校园,其他学生不能通过这个门进入校园,但是其他学生也可以通过其它的门进入。


recvfrom

绑定好主机后,就可以开始进行网络通信了,UDP基于报文通信,所以发送的数据也是报文形式。

recvfrom函数用于接收数据报,需要头文件<sys.types.h><sys/socket.h>,函数原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd:套接字的文件描述符
  • buf:指向接收缓冲区
  • len:最多可以读取的字节数,一般防止接收缓冲区越界
  • flags:控制发送行为的标志,通常设为 0
  • src_addr:输出型参数,得到消息发送方的sockaddr,即IP地址和端口号
  • addrlen:输出型参数,得到src_addr 真实结构的大小

返回值:

  • 成功时返回实际读取的字节数
  • 失败时返回 -1,并设置错误码

示例:

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

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

bind(sockfd, (struct sockaddr*)&src_addr, sizeof(src_addr));

// 输出形参数
struct sockaddr_in peer;
socklen_t peer_len;

// 接收消息
char* buf[1024];
recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&peer, &peer_len);

此处的peer用于接收发送方的sockaddr,也就是IP地址和端口号。发送方发送的报文被存储在buf中。


sendto

sendto函数用于发送数据报,需要头文件<sys.types.h><sys/socket.h>,函数原型如下:

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

参数:

  • sockfd:套接字的文件描述符
  • buf:指向要发送的数据缓冲区的指针
  • len:要发送的数据的字节数
  • flags:控制发送行为的标志,通常设为 0
  • dest_addr:指向消息接收方目标地址sockaddr的指针
  • addrlendest_addr 真实结构的大小。

返回值:

  • 成功时返回实际发送的字节数
  • 失败时返回 -1,并设置错误码
  • 返回0表示没有读取到数据

示例:

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

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

const char *message = "Hello, UDP!";
sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));

此处给192.168.1.100地址,通过端口8888发送了一个报文,内容是”Hello, UDP!“

不知你有没有发现这一份代码有一点蹊跷,这个套接字没有bind。此处程序作为客户端向主机主动发起报文,那么操作系统会给这个进程分配一个端口。在还没有发送数据前,我们并不知道这个随机分配的端口是多少,无法进行bind,这该咋办?

在发送数据时,有两种情况:

  1. 如果套接字没有绑定端口号,Linux会自动为其分配端口号,并完成绑定,随后通过随机分配的端口发送数据,这种行为称为隐式绑定
  2. 如果套接字已经绑定了端口号,Linux则直接通过指定端口发送数据

因此以上代码中没有bind这个过程。


TCP socket

socket

与UDP一样,TCP也要通过socket函数创建套接字,只是参数略有不同。

int socket(int domain, int type, int protocol);
  • domain:对于IPv4,使用AF_INET
  • type:填入SOCK_STREAM,即面向字节流的TCP服务
  • protocol:填0即可

创建TCP套接字代码如下:

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

后续就可以通过文件描述符sockfd操作TCP连接了。


bind

同样的,TCP服务端要通过bind进行绑定,表明自己连接的主机以及端口号。而客户端不用,因为发送数据时操作系统会隐式绑定。


listen

当TCP服务端启动后,此时就要等待别人来连接自己,此时就处于listen状态。而进入该状态,需要调用函数listen,包含在头文件<sys/types.h><sys/socket.h>,函数原型如下:

int listen(int sockfd, int backlog);

参数:

  • sockfd:进行监听的套接字
  • backlog:TCP全连接队列的大小,此处暂时不管,设为一个适中的值,比如10

返回值:

  • 成功:返回0
  • 失败:返回-1

示例:

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

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

bind(sockfd, (struct sockaddr*)&sock, sizeof(sock));
listen(sockfd, 10);

while (true);

假设该进程为test.exe,执行该程序后,通过netstat指令查看:

在这里插入图片描述

此时进程就处于LISTEN状态,即等待连接。


connect

对于客户端来说,需要向服务端发起连接请求,此时需要函数connect,包含在头文件<sys/types.h><sys/socket.h>中,函数原型如下:

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

参数:

  • sockfd:发起连接的套接字
  • addr:发起连接对象的信息,即IP地址何端口号
  • addrlenaddr的真实类型的大小

返回值:

  • 成功:返回0
  • 失败:返回-1并设置错误码

以下是一个客户端的示例:

// 构建目标主机信息
struct sockaddr_in sock;
bzero(&sock, sizeof(sock));
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = inet_addr("127.0.0.1");
sock.sin_port = htons(8888);

// 发起TCP连接
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, (struct sockaddr*)&sock, sizeof(sock));

accept

当服务端在listen状态下,接收到来自客户端的连接,就可以选择同意这个连接,此时需要函数accept,包含在头文件<sys/types.h><sys/socket.h>中,函数原型如下:

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

参数:

  • sockfd:之前进行listen的套接字
  • sockaddr:输出客户端的信息,即IP地址和端口号
  • addrlensockaddr的真实大小

返回值:

  • 成功:返回一个文件描述符
  • 失败:返回-1并设置错误码

奇怪的事情来了,为什么accept会返回一个文件描述符?

此处的文件描述符,其实就是一个套接字socket,先前说过套接字是通过文件描述符来操作的。此处返回的套接字,是专门用于和客户端通信的套接字。也就是说后续与客户端通信,使用这个新的套接字完成。

对于TCP服务端来说,有两种套接字,一种是用于listen的套接字,其负责监听指定端口,查看有没有到来的连接。一旦连接建立成功,此时与客户端的通信过程由新的套接字完成。

示例:

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

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

bind(listen_sockfd, (struct sockaddr*)&sock, sizeof(sock));
listen(listen_sockfd, 10);

// peer用于接收客户端信息
struct sockaddr_in peer;
socklen_t peer_len;

// 接收来自客户端的连接
int sockfd = accept(listen_sockfd, (struct sockaddr*)&peer, &peer_len);

以上是一个完整的TCP服务端启动过程,一般而言accpet之后,会使用多进程/多线程完成后续的通信,而主进程继续listen其它的连接,本博客不展示该过程了。

此处的listen_sockfd就是专门用于连接的套接字,而最后的sockfd 是与客户端通信的套接字。


send

当连接建立成功后,就可以开始收发消息了,TCP是面向字节流的,与UDP不同,TCP可以把sockfd文件描述符完全当作一个文件,完成消息的读写。

发送消息使用send函数,包含在头文件<sys/types.h><sys/socket.h>中,函数原型如下:

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

参数:

  • sockfd:用于通信的套接字
  • buf:要发送的数据缓冲区
  • len:要发送的数据的长度
  • flags:设为0即可

不知道你有没有发现,它和文件写入函数write几乎没有差别:

ssize_t write(int fd, const void *buf, size_t count);

前三个参数没有区别,而send的第四个参数固定为0。其实sendwrite一样,都是直接向文件中写入字符串的,这符合TCP面向字节流的特性,在发送数据时,也可以用write代替send


recv

接收消息使用recv函数,包含在头文件<sys/types.h><sys/socket.h>中,函数原型如下:

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

参数:

  • sockfd:用于通信的套接字
  • buf:数据的接收缓冲区
  • len:最大接收数据的长度
  • flags:设为0即可

同样,其实recvread函数差不多:

ssize_t read(int fd, void *buf, size_t count);

在读取TCP连接的数据的时可以使用两者的任何一个。


标签:sockaddr,UDP,socket,sock,struct,TCP,接字,sin,addr
From: https://blog.csdn.net/fsdfafsdsd/article/details/141554944

相关文章

  • TCP并发服务器多线程和多进程方式以及几种IO模型
    1.阻塞I/O(BlockingI/O)在阻塞I/O模型中,当应用程序发起I/O操作时,整个进程会被阻塞,直到操作完成。在这个过程中,应用程序无法执行其他任务,必须等待I/O操作的完成。特点:简单性:编程简单,逻辑清晰,容易理解和实现。低效性:在高并发场景下,由于每个I/O操作都会阻塞整个进程,资......
  • 如何使用 Bittly 实现 UDP 请求自动响应与处理
    在开发基于UDP的应用时,如果通信目标未就绪或者临时不可用时,可以使用Bittly的模拟服务虚拟一个支持UDP通讯的通讯终端。本文将介绍如何使用Bittly工具,实现对UDP请求的自动响应、动态数据处理、数据分帧以及数据转发。我们将从服务的准备工作开始,逐步讲解每一个步骤,帮......
  • Fins TCP协议理解及C Sharp实现思路
     假设本文中使用到设备的ip地址,用于后续内容的理解:客户端(本机电脑windows系统)IP:192.168.1.101服务端(PLComronCJ2M系列)IP和端口号:192.168.1.10:9600 注意:①本文中的FINSTCP报文都是以16进制(Hex)发送出去的,所以对应的转换也都会转成16进制的形式。②16进制He......
  • Websocket
    一、基础知识全双工、半双工和单工是通信系统中的三种数据传输模式,它们描述了数据在通信链路上的传输方式和方向。全双工:数据可以同时在两个方向传输,双方可以同时发送和接收数据。这是双向通信最先进的模式,允许同时进行的双向交流。半双工:数据可以在两个方向上传输,但不能同时......
  • 网络通信和TCP/IP协议详解
    目录网络协议一、计算机网络是什么?定义和分类计算机网络发展简史二、计算机网络体系结构OSI七层模型TCP/IP模型TCP/IP协议族IP、TCP和UDPTCP/IP网络传输中的数据地址和端口号MAC地址IP地址端口号综述三、TCP特性TCP三次握手为什么TCP握手需要三......
  • 【实践经验】端口被占用问题:listen tcp:bind:only one usage of each socket address
    文章目录一.问题描述二.分析1.适用错误三.解决方法1.打开控制台2.查看端口的使用情况2.1不知道端口号——查看所有运行的端口2.2知道端口号3.查看使用进程的程序4.杀死进程5.验证端口是否释放一.问题描述goland启动项目后报错:“listentcp:bind:onl......
  • 【网络编程通关之路】 Udp 基础回显服务器(Java实现)及你不知道知识原理详解 ! ! !
    本篇会加入个人的所谓鱼式疯言❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言而是理解过并总结出来通俗易懂的大白话,小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.......
  • 网络编程-Socket通信
    Socket通信1、什么是套接字​ Socket是封装了TCP/IP协议簇的系统API接口,这使得程序员无需关注协议本身,直接使用socket提供的接口与不同主机间的进程互联通信。​ 目前市面上主流的操作系统都采用这套机制进制网络通信,所以不同种类的操作系统,使用不同的编程语言,只要调用操作系统......
  • 搭建多协议的串口服务器流程:RS-232、RS-485和TCP/IP、MQTT网络协议(代码示例)
    一、项目概述在物联网(IoT)和自动化控制的快速发展中,串口通信作为一种经典的通信方式,依然发挥着重要作用。本项目旨在构建一个支持多种协议的串口服务器,能够通过串口接收和发送数据,并通过网络协议(如TCP/IP、MQTT等)与其他设备和系统进行交互。项目的目标和用途本项目的目标......
  • 【计算机网络】WebSocket协议
    目录一、WebSocket协议概述二、WebSocket协议基本特点三、WebSocket协议代码实现3.1 WebSocket协议python实现3.2 WebSocket协议JAVA实现3.3 WebSocket协议C++实现四、WebSocket协议发展趋势一、WebSocket协议概述        WebSocket协议是一种在单个TCP......