Linux基础-socket详解、TCP/UDP

发布于:2024-04-30 ⋅ 阅读:(23) ⋅ 点赞:(0)


一、Socket 介绍

所谓socket通常也称作 “套接字”,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过 “套接字” 向网络发出请求或者应答网络请求 。
Socket 是计算机网络编程中一个重要的概念,它是在应用层和传输层之间提供的一种抽象接口,用于实现应用程序之间的数据交换。Socket 允许程序员使用一种通用的接口来访问底层传输协议,如 TCP 和 UDP,以便进行网络通信。
Socket 是一种编程接口,它提供了一种标准化的方式来创建网络连接,并允许应用程序在网络上发送和接收数据。Socket API 提供了一组函数,这些函数可以用于创建和配置套接字,建立连接,发送和接收数据,以及关闭连接等操作。
Socket 包括流式 Socket(基于 TCP)数据报式 Socket(基于 UDP),其中,TCP面向连接,可靠性较高,可实现有序、零差错的传输;而 UDP 不能确保传输的数据按序、及时到达,因此,UDP 主要出现在数据可靠性要求较低的场合。常见的 Socket 编程相关术语进行介绍:

相关术语 介绍
IP 地址 唯一标识了网络上的一台主机
端口号 是一个 16 位数字,用于标识一个应用程序在主机上的具体位置
套接字 用于标识一条网络连接的两端,包含 IP 地址和端口号
服务器 在网络上提供服务的主机
客户端 与服务器进行通信的主机

一 个Socket 主要由5个信息所构成,分别为:协议,本地地址,本地端口号,远地地址,远地端口号。

  • 协议指定了socket所使用的的通讯协议,一般有TCP或者UDP等。
  • 本地IP地址即本地主机的地址。
  • 本地端口号用以和本地运行的其他程序所区分。
  • 远地IP地址即依照网络协议分配给远程主机的网络地址。
  • 远地端口号用以和远程主机运行的其他程序所区分。

二、Socket 通信模型

Socket 编程通常分为两个部分:服务器端和客户端
服务器端监听一个指定的端口,等待客户端连接。一旦客户端连接到服务器端,服务器端将创建一个新的套接字来处理该客户端的请求。服务器可以同时处理多个客户端请求,每个客户端都会有自己的套接字连接。
客户端首先创建一个套接字,然后连接到服务器端的指定 IP 地址和端口号。一旦连接建立,客户端可以通过套接字发送和接收数据。客户端通常是一次性连接,一旦任务完成就关闭套接字。
在这里插入图片描述
Socket 编程可以用于各种不同的应用程序,例如聊天程序、文件传输、在线游戏等。Socket编程还可以用于创建网络服务器,提供 Web 服务、FTP 服务、邮件服务等。

三、Socket 常用函数

socket 编程的系统调用常用函数如下所示:

  • 创建套接字 socket(domain, type, protocol)
  • 绑定套接字 bind(sockfd, addr, addrlen)
  • 监听连接 listen(sockfd, backlog)
  • 接受连接 accept(sockfd, addr, addrlen)
  • 接收和发送数据 recv(sockfd, buf, len, flags)、send(sockfd, buf, len, flags)
  • 关闭套接字 close(sockfd)

1 创建套接字

在 Linux socket 编程中,创建套接字是构建网络应用程序的第一步。套接字可以理解为应用程序和网络之间的桥梁,用于在网络上进行数据的收发和处理。在 Linux 中,可以使用 socket 系统调用创建套接字。该系统调用的原型和所需头文件如下所示:

#include <sys/types.h>
#include <sys/socket.h>
int socket( (int domain, int type, int protocol );

其中,domain 参数指定了套接字的协议族,type 参数指定了套接字的类型,protocol 参数指定了套接字所使用的具体协议。下面分别介绍这三个参数的含义:

  • 协议族
    协议族指定了套接字所使用的协议类型,常用的协议族包括 AF_INET、AF_INET6、AF_UNIX等。其中,AF_INET 表示 IPv4 协议族,AF_INET6 表示 IPv6 协议族,AF_UNIX 表示 Unix 域协议族。
  • 套接字类型
    套接字类型指定了套接字的数据传输方式,常用的套接字类型包括 SOCK_STREAM、SOCK_DGRAM、SOCK_RAW 等。其中,SOCK_STREAM 表示面向连接的流套接字,主要用于可靠传输数据,例如 TCP 协议。SOCK_DGRAM 表示无连接的数据报套接字,主要用于不可靠传输数据,例如 UDP 协议。SOCK_RAW 表示原始套接字,可以直接访问底层网络协议。
  • 协议类型
    协议类型指定了套接字所使用的具体协议类型,常用的协议类型包括 IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP 等。其中,IPPROTO_TCP 表示 TCP 协议,IPPROTO_UDP 表示 UDP协议,IPPROTO_ICMP 表示 ICMP 协议,通常使用 0 表示由系统自动选择适合的协议。
    例如可以使用以下代码创建一个新的套接字:
int sockfd = = socket(AF_INET, SOCK_STREAM, 0);

2 绑定套接字

创建套接字后,需要将其与一个网络地址绑定,以便其他计算机可以访问该套接字。在 Linux系统下,可以使用 bind()系统调用绑定套接字和地址。该系统调用的原型和所需头文件如下所示:

#include <sys/types.h>
#include <sys/socket.h>
int bind( (int sockfd, const struct sockaddr *addr, socklen_t addrlen);

其中,sockfd 参数指定了需要绑定的套接字描述符,addr 参数指定了需要绑定的地址信息,可以是 struct sockaddr_in 或 struct sockaddr_in6 等结构体类型,addrlen 参数指定了地址信息的长度。使用以下代码将套接字和地址绑定:

// bind port 
// 创建一个 sockaddr_in 结构体类型的 servaddr 变量用于存储服务器的地址信息,并将其清零。
struct sockaddr_in bindaddr;
memset (&bindaddr, 0, sizeof(bindaddr);
bindaddr.sin_family = AF_INET;//指定使用 IPv4 协议(AF_INET)
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地任意可用的 IP 地址(INADDR_ANY)
bindaddr.sin_port = htons(3000);//使用指定的端口号(port)
// 将套接字 listenfd 绑定到指定的地址 bindaddr上,bind() 函数返回值为 0 表示绑定成功
if(0 != bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr))) {
	printf("bind error\n");
	return -1;
}

在初始化 sockaddr_in 结构体时,用到了以下函数:

  • memset() :用来将 sockaddr_in 结构体的各个成员变量初始化为 0,这是一个常用的初始化方式。其函数原型如下:
 #include <string.h> 
 // s 参数是需要被初始化的内存区域指针,c 参数是填充字符,n 参数是需要填充的内存字节数。
 void *memset( void *s, int c, size_t n ); 
  • htonsl()和 和 htons():用于将本机字节序的端口号转换为网络字节序的端口号。其函数原型如下:
 #include <arpa/inet.h> 
uint32_t htonl(uint32_t hostlong );
uint16_t htons(uint16_t hostshort);

其中,hostshort 和 hostlong 参数是本机字节序的端口号,函数返回值是网络字节序的端口号。

3、监听连接

绑定套接字后,需要开始监听连接请求,以便其他网络上的客户端能够与该套接字建立连接。这一步骤通常在服务器端完成。在 Linux 系统下,可以使用 listen()系统调用监听套接字。该系统调用的原型如下:

#include <sys/socket.h> 
int listen(int socket, int backlog);

其中,sockfd 参数指定了需要监听的套接字描述符,backlog 参数指定了连接队列的长度,即等待接受的连接数。例如可以使用以下代码开始监听连接.

// start listen
if (listen(listenfd, 2) != 0) {
	printf("listem error\n");
	return -1;
}

4、接受连接

当有客户端请求连接时,需要接受该连接并进行处理。在 Linux 系统下,可以使用 accept(),系统调用接受连接请求。该系统调用的原型和所需头文件如下所示:

#include <sys/socket.h>
int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);

其中,sockfd 参数指定了需要接受连接的套接字描述符,addr 参数用于存储客户端的地址信息,addrlen 参数用于存储地址信息的长度。使用以下代码接受连接请求:

// 定义一个 sockaddr_in 结构体,用于存储客户端的 IP 地址和端口号
struct sockaddr_in clientaddr;
// 定义一个 socklen_t 类型的变量 clientaddrlen ,用于存储客户端地址结构体的长度
socklen_t clientaddrlen = sizeof(clientaddr);

// 调用 accept() 系统调用,接受客户端的连接请求,并返回一个新的套接字描述符 connfd,用于与客户端进行通信。
// accept() 函数会阻塞程序,直到有客户端连接到服务器端。
// listenfd 是服务端的监听套接字, clientaddr 是指向 sockaddr_in 结构体的指针,用于存储客户端的地址信息。
// clientaddrlen 是客户端地址结构体的长度,accept() 函数会将实际接受到的客户端地址长度存储到该变量中。
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd >= 0) { // 判断 accept() 函数的返回值,判断客户端连接是否失败
	// 处理接收数据
	
} else{
	printf("accept error\n");
}

5、接收和发送数据

在 Linux socket 编程中,接收和发送数据是套接字编程的核心步骤之一。当套接字绑定并且处于监听状态,已经成功接受了客户端的连接请求后,接下来就是进行数据的收发。下面将详细介绍如何在 Linux 系统中实现接收和发送数据的过程。

接收数据

在接收数据之前,需要先了解数据在网络传输中的一些基本概念。在 TCP 协议中,发送方发送的数据被分割成一个个 TCP 报文段,每个报文段都包含一个 TCP 首部和数据部分。TCP首部中包含了一些控制信息,如序号、确认号、窗口大小等,用来保证数据的可靠传输。而数据部分则是发送方发送的应用层数据,如 HTTP 报文、FTP 文件等。
在 Linux socket 编程中,接收数据的过程分为两步:先接收 TCP 首部,再接收数据部分。具体步骤如下:
(1)创建一个缓冲区用于接收数据。缓冲区的大小一般为数据部分的大小。
(2)调用 recv() 系统调用接收数据。其函数原型和所需头文件如下:

#include <sys/socket.h> 
ssize_t recv(int sockfd, void *buffer, size_t length, int flags );

其中,sockfd 参数是需要接收数据的套接字描述符,buf 参数是用于存储接收数据的缓冲区指针,len 参数是需要接收的数据的最大长度,flags 参数是接收标志,通常为 0。函数返回值为实际接收到的字节数,如果返回值为 0,表示对端已经关闭连接。
(3)在调用 recv() 系统调用时,会先接收 TCP 首部,然后再接收数据部分。如果数据部分比较大,可能需要多次调用 recv() 系统调用才能接收完整的数据。因此,需要使用一个循环来不断接收数据,直到接收到全部数据或者出现错误为止。另外,需要注意的是,recv() 系统调用是一个阻塞调用,即程序会一直等待直到接收到数据或者出现错误才会返回。如果不希望阻塞调用,可以使用非阻塞 I/O 或者多路复用技术。
(4) 处理接收到的数据。在数据接收完成后,需要对接收到的数据进行处理。具体处理方式根据具体的应用场景而定,如将接收到的数据显示在终端上、将数据写入文件等。

发送数据

发送数据的步骤如下:
(1)定义数据缓冲区:需要定义一个用于存储待发送数据的缓冲区,比如 char buf[MAXLINE]。
(2)将数据拷贝到缓冲区:将需要发送的数据拷贝到缓冲区中,可以使用 strcpy()、memcpy() 等函数进行拷贝操作。
(3)使用 send() 函数发送数据:send() 函数用于向已经建立连接的套接字发送数据,其函数原型如下:

#include <sys/socket.h> 
ssize_t send( int sockfd, const void *buf, size_t len, int flags);

函数调用成功将返回发送数据的字节数(返回值为非负数),返回-1 表示发送失败。sockfd 需要发送数据的套接字描述符;buf 待发送数据的缓冲区指针;len 待发送数据的长度;flags 传输标志,通常为 0。

// send data
int ret = send(clientfd, buf, strlen(buf), 0);
if (ret != strlen(buf)){
	printf("send data error\n");
	return -1;
}

需要注意的是,send() 函数并不保证一次能够将所有数据都发送出去,如果数据量比较大,可能需要多次调用 send() 函数才能将所有数据发送出去。
(4)判断是否发送完毕:可以通过判断 send() 函数的返回值和待发送数据的长度是否相等来确定是否发送完毕。

6、关闭套接字

关闭套接字是一个非常重要的步骤。当套接字不再需要使用时,应该立即关闭以释放系统资源和避免资源浪费。关闭套接字的步骤非常简单,只需要调用 close() 系统调用即可。close()
系统调用的函数原型如下:

int close( (int sockfd );

其中,sockfd 参数是需要关闭的套接字描述符。函数返回值为 0 表示成功,返回值为 -1表示失败。

四、Socket编程试验

1、源码

server.c

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
// 自定义传输的数据
typedef struct  msgbuf{
	int id;
	char str[32];
} msgbuf;

int main() {
	
	// create socket
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == listenfd) {
		printf("create socket error");
		return -1;
	}
    printf("server create socket ok!\n");
	
	// bind port 
	// 创建一个 sockaddr_in 结构体类型的 servaddr 变量用于存储服务器的地址信息,并将其清零。
	struct sockaddr_in bindaddr;
	memset (&bindaddr, 0, sizeof(bindaddr));
	bindaddr.sin_family = AF_INET;//指定使用 IPv4 协议(AF_INET)
	bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地任意可用的 IP 地址(INADDR_ANY)
	bindaddr.sin_port = htons(3000);//使用指定的端口号(port)
	// 将套接字 listenfd 绑定到指定的地址 bindaddr上,bind() 函数返回值为 0 表示绑定成功
	if(0 != bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr))) {
		printf("bind error\n");
		return -1;
	}
	
	// start listen
	if (listen(listenfd, 2) != 0) {
		printf("listem error\n");
		return -1;
	}

    msgbuf socmsg;
	// 定义一个 sockaddr_in 结构体,用于存储客户端的 IP 地址和端口号
	struct sockaddr_in clientaddr;
	// 定义一个 socklen_t 类型的变量 clientaddrlen ,用于存储客户端地址结构体的长度
	socklen_t clientaddrlen = sizeof(clientaddr);

	// accept connection
	// 调用 accept() 系统调用,接受客户端的连接请求,并返回一个新的套接字描述符 connfd,用于与客户端进行通信。
	// accept() 函数会阻塞程序,直到有客户端连接到服务器端。
	// listenfd 是服务端的监听套接字, clientaddr 是指向 sockaddr_in 结构体的指针,用于存储客户端的地址信息。
	// clientaddrlen 是客户端地址结构体的长度,accept() 函数会将实际接受到的客户端地址长度存储到该变量中。
	int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
	if (clientfd >= 0) { // 判断 accept() 函数的返回值,判断客户端连接是否失败
		while (1) {        	
			socmsg.id = 0;
			memset(socmsg.str, ' ',sizeof(socmsg.str));
			
			// receive data
			int ret = recv(clientfd, &socmsg, sizeof(socmsg), 0);
			if (ret > 0) {
				printf("recv data from client, data: %d %s\n", socmsg.id, socmsg.str);
				
				printf("send data ...\n");
				// send data
				ret = send(clientfd, &socmsg, sizeof(socmsg), 0);
				if (ret != sizeof(socmsg)) {
					printf("send data error.\n");
				}
				
			} else {
				// printf("recv data error.\n");
				sleep(1);
			}
		}		
		close(clientfd);
	} else{
		printf("accept error\n");
	}

	
	//close socket
	close(listenfd);
	return 0;
}

client.c

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

typedef struct  msgbuf{
	int id;
	char str[32];
} msgbuf;

int main(int argc, const char *argv[]) {
	
	char *ip = "192.168.1.100"; // 服务器默认的IP,可以改成服务器实际IP

	// create socket
	int clientfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == clientfd) {
		printf("create socket error");
		return -1;
	}
    printf("client create socket ok!\n");
	
	// connect server 
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;

	if (argc == 2) { // 如果有输入IP参数,使用输入的IP参数
		printf("%s\r\n", argv[1]);
		serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // 从命令行参数中获取服务器 IP 地址
	} else {
    	serveraddr.sin_addr.s_addr = inet_addr(ip);
	}

        serveraddr.sin_port = htons(3000);
	
	if(-1 == connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) {
		printf("connect error\n");
		return -1;
	}
	printf("connect OK\n");

	msgbuf socmsg;
	int i =0;

	while(1) {
		sleep(1);
		socmsg.id = i++;

		memcpy(socmsg.str,"this is my test msg", strlen("this is my test msg"));
		
		// send data
		int ret = send(clientfd, &socmsg, sizeof(socmsg), 0);
		if (ret != sizeof(socmsg)){
			printf("send data error\n");
			return -1;
		}
		printf("send data ok, %d %s\n",socmsg.id, socmsg.str);
		
		// receive data
		socmsg.id = 0;
		// socmsg.str[128] = {0};
		memset(socmsg.str, ' ',sizeof(socmsg.str));

		ret = recv(clientfd, &socmsg, sizeof(socmsg), 0);
		if (ret > 0) {
			printf("receive data from server: %d %s\n", socmsg.id, socmsg.str);
		} else {
			printf("receive data error\n");
		}
	}
	// close socket
	close(clientfd);

    return 0;
}

2、编译:

gcc -o server server.c
gcc -o client client.c

如果client是在板子上执行,需要使用板子系统对应的交叉编译器来编译。例如:aarch64-linux-gnu-gcc -o client client.c

3、执行结果

在这里插入图片描述
在这里插入图片描述
本例程只作为一个简单的demo,体验下socket通讯。

五、补充TCP和UDP协议的Socket流程对比

在一般的网络书籍中,网络协议被分为 5 层:
在这里插入图片描述
⚫应用层:它是体系结构中的最高层,直接为用户的应用进程(例如电子邮件、文件传输和终端仿真)提供服务。在因特网中的应用层协议很多,如支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议,支持文件传送的 FTP , DNS,POP3, SNMP, Telnet 等等。
⚫运输层:负责向两个主机中进程之间的通信提供服务。
运输层主要使用以下两种协议:
① 传输控制协议 TCP(Transmission Control Protocol):面向连接的,数据传输的单位是报文段,能够提供可靠的交付。
② 用户数据包协议 UDP(User Datagram Protocol):无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”。
⚫ 网络层:负责将被称为数据包(datagram)的网络层分组从一台主机移动到另一台主机。
⚫ 链路层:因特网的网络层通过源和目的地之间的一系列路由器路由数据报。
⚫ 物理层:在物理层上所传数据的单位是比特。物理层的任务就是透明地传送比特流。
这些层对于初学者来说很难理解,我们只需要知道:我们需要使用“运输层”编写应用程序,我们的应用程序位于“应用层”。使用“运输层”时,可以选择 TCP 协议,也可以选择 UDP 协议。

1、TCP 和 和 UDP区别

TCP 向它的应用程序提供了面向连接的服务。这种服务有 2 个特点:可靠传输、流量控制(即发送方/接收方速率匹配)。它包括了应用层报文划分为短报文,并提供拥塞控制机制。
UDP 协议向它的应用程序提供无连接服务。它没有可靠性,没有流量控制,也没有拥塞控制。
在这里插入图片描述

既然 TCP 提供了可靠数据传输服务,而 UDP 不能提供,那么 TCP 是否总是首选呢?

答案是否定的,因为有许多应用更适合用 UDP,举个例子:视频通话时,使用 UDP,偶尔的丢包、偶尔的花屏时可以忍受的;如果使用 TCP,每个数据包都要确保可靠传输,当它出错时就重传,这会导致后续的数据包被阻滞,视频效果反而不好。使用 UDP 时,有如下特点:
① 关于何时发送什么数据控制的更为精细
采用 UDP 时只要应用进程将数据传递给 UDP,UDP 就会立即将其传递给网络层。而 TCP 有重传机制,而不管可靠交付需要多长时间。但是实时应用通常不希望过分的延迟报文段的传送,且能容忍一部分数据丢失。
② 无需建立连接,不会引入建立连接时的延迟。
③ 无连接状态,能支持更多的活跃客户。
④ 分组首部开销较小。

2、TCP工作流程图

在这里插入图片描述

3、UDP工作流程图

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到