Linux:socket套接字介绍及实现简单的udp通信


socket套接字编程

socket是一套网络编程接口,类似于中间件;上层用户可以通过这些接口简单的完成网络通信传输;而不需要过于关心内部的实现过程。套接字编程就是通过socket接口实现网络通信

通常情况下程序员接所接触到的套接字(Socket)为两类:

  • 流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的TCP 服务应用;
  • 数据报式套接字(SOCK_DGRAM):一种无连接的Socket,对应于无连接的UDP 服务应用。

从用户的角度来看,SOCK_STREAM、SOCK_DGRAM 这两类套接字似乎的确涵盖了TCP/IP 应用的全部,因为基于TCP/IP 的应用,从协议栈的层次上讲,在传输层的确只可能建立于TCP 或 UDP协议之上,而SOCK_STREAM、SOCK_DGRAM 又分别对应于TCP和UDP,所以几乎所有的应用都可以用这两类套接字实现。

在这里插入图片描述

socket编程:tcp/dup

传输层的两个协议:tcp/udp(这两个协议特性各有不同,因此实现流程也稍有差别)

  • tcp:传输控制协议:面向连接,可靠传输,面向字节流

应用场景: 数据安全性大于实时性的场景 - 文件传输

面向字节流:基于连接的,可靠的,有序的,双向的字节流传输服务(以字节为单位,不限制上层传输数据大小的方式)

  • udp:用户数据报协议:无连接,不可靠,面向数据报

应用场景:

就是数据实时性大于安全性的场景 - 视频传输

面向数据报:无连接的,不可靠的,无序的,有最大长度限制的数据传输服务

网络通信

网络中的两端主机上的进程之间的通信;这两端有个名称:客户端/服务器端

  • 客户端:是主动发出请求的一方主机
  • 服务器端:是被动接受请求的一方主机

注意:

永远都是客户端主机先向服务器端发送请求 :

  • udp是不可靠的,不关心是否响应,不关心数据是否丢失了
  • tcp是可靠传输,需要先建立连接,若服务器不响应,则连接都无法建立

这意味着客户端必须知道服务端的地址信息才可以在发送数据的时候,将数据能够层层封装完成(网络传输的数据都应该包含:源IP地址/目的IP地址/源端口/目的端口/协议)

  • 服务端只能绑定的是服务端主机上的IP地址;客户端也绑定的是自己主机上的IP地址

网络字节序和主机字节序的转换

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

在这里插入图片描述

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

地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换;

字符串转in_addr的函数:

#include <arpa/inet.t>

uint32_t inet_addr(const char *ip); --- 将点分十进制字符串IP地址转换为网络字节序整数IP地址
int inet_pton(int domain, char *src, void *dst); --- 将字符串src的IP地址按照domain地址域转换网络字节序的IP地址

in_addr转字符串的函数:

char *inet_ntoa(struct in_addr addr); --- 网络字节序整数IP地址转换为点分十进制字符串
int inet_ntop(int domain, void *src, char *dst, int len); --- 将网络字节序数据按照domain地址域转换为字符串IP地址

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void * addrptr。

关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

在这里插入图片描述

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

udp 网络通信程序编程流程

在这里插入图片描述

socket 编程接口介绍

  1. 创建套接字
int socket(int domain, int type, int protocol);

参数:

  • domain: 地址域 - 不同的网络地址结构 AF_INET - IPv4地址域
  • type: 套接字类型 - 流式套接字/数据报套接字
    • 流式套接字(SOCK_STREAM):一种有序的,可靠的,双向的,基于连接的字节流传输
    • 数据报套接字(SOCK_DGRAM):无连接的,不可靠的,有最大长度限制的传输
  • protocol: 使用的协议( 0 - 不同套接字类型下的默认协议:流式套接字默认是tcp;数据报套接字默认是udp)
    • IPPROTO_TCP - tcp协议
    • IPPROTO_UDP - udp协议

返回值: 返回套接字的操作句柄(文件描述符)

  1. 为套接字绑定地址信息
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);

参数:

  • sockfd: 创建套接字返回的操作句柄
  • address: 要绑定的地址信息结构
  • address_len: 地址信息的长度

返回值: 成功返回0;失败返回-1

用户先定义sockaddr_in的IPv4地址结构,强转之后传入bind之中

bind(sockaddr*){
    if (sockaddr->sin_family == AF_INET){
        这是ipv4地址结构的解析
    }else if(sockaddr->sin_family == AF_INET6){
        这是ipv6地址结构的解析
    }
}
  1. 发送数据
int sendto(int sockfd, char * data, int data_len, int flag, struct sockaddr *dest_addr, socklen_t addr_len);

参数:

  • sockfd: 套接字操作句柄。(发送数据就是将数据拷贝到内核的socket发送缓冲区中)

  • data: 要发送的数据的首地址

  • data_len: 要发送的数据的长度

  • flag: 选项参数

    • 默认为0 - 表示当前操作是阻塞操作
    • MSG_DONTWAIT - 设置为非阻塞

    若发送数据的时候,socket发送缓冲区已经满了,则0默认阻塞等待;MSG_DONTWAIT就是立即报错返回了

  • dest_addr: 目的端地址信息结构(表示数据要发送给谁)
    每一条数据都要描述源端信息(绑定的地址信息)和对端信息(当前赋予的信息)

  • addr_len: 地址信息结构长度

返回值: 成功返回实际发送的数据字节数;失败返回-1

  1. 接收数据
int recvfrom(int sockfd, char *buf, int len, int flag, struct sockaddr *src_addr, socklen_t *addr_len);

参数:

  • sockfd: 套接字操作句柄
  • buf: 缓冲区的首地址(用于存放接收到的数据,从内核socket接收缓冲区中取出数据放入这个buf用户态缓冲区中)
  • len:用户想要读取的数据长度,但是不能大于buf缓冲区的长度
  • flag:
    • 0 - 默认阻塞操作(若缓冲区中没有数据则一直等待)
    • MSG_DONTWAIT - 非阻塞
  • src_addr:接受到的数据的源端地址 - 表示这个数据是谁发的,从哪来的(回复的时候就是对这个地址回复)
  • addr_len: 输入输出型参数,用于指定想要获取多长的地址信息;获取地址之后,用于返回地址信息的实际长度

返回值:成功返回实际接收到数据字节长度;失败返回-1

  1. 关闭套接字
int close(int fd);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构
在这里插入图片描述

sockaddr_in 结构

在这里插入图片描述

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构
在这里插入图片描述

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

实现一个简单的udp客户端/服务端

  • udp客户端程序

C++封装一个udpsocket类,向外提供简单接口就能实现一个客户端/服务端

代码示例

// 这个demo封装一个udpsocket类,向外提供简单的接口实现套接字编程

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>	// close接口
#include <stdlib.h>	// atio接口
#include <netinet/in.h>	// 地址结构定义
#include <arpa/inet.h>	// 字节序转换接口
#include <sys/socket.h>	// 套接字接口

class UdpSocket{
public:
	UdpSocket():_sockfd(-1){
	}
	// 1.创建套接字
	bool Socket(){
		_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if(_sockfd < 0){
			perror("socket error");
			return false;
		}
		return true;
	}
	// 2.为套接字绑定地址信息
	bool Bind(const std::string &ip, uint32_t port){
		// 1.定义IPv4地址结构
		struct sockaddr_in addr;
		addr.sin_family = AF_INET;
		addr.sin_port = htons(port);
		addr.sin_addr.s_addr = inet_addr(ip.c_str());
		// 2.绑定地址
		socklen_t len = sizeof(struct sockaddr_in);
		// bind(描述符, 统一地址结构sockaddr*, 地址信息长度)
		int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("bind error");
			return false;
		}
		return true;
	}
	// 3.发送数据
	bool Send(const std::string &data, const std::string &ip, uint16_t port){
		// sendto(描述符,数据,长度,选项,对端地址,地址长度)
		// 1.定义对端地址信息的IPv4地址结构
		struct sockaddr_in addr;
		addr.sin_family = AF_INET;
		addr.sin_port = htons(port);
		addr.sin_addr.s_addr = inet_addr(ip.c_str());
		// 2.向这个地址发送数据
		socklen_t len = sizeof(struct sockaddr_in);
		int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("sendto error");
			return false;
		}
		return true;
	}
	// 输入型参数使用 const 引用;输出型参数使用 指针;输入输出型使用引用
	// 4.接收数据
	bool Recv(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL){
		// recvfrom(描述符,缓冲区,数据长度,选项,对端地址,地址长度)
		struct sockaddr_in addr;	// 用于获取发送端地址信息
		socklen_t len = sizeof(struct sockaddr_in);	// 指定地址长度以及获取实际地址长度
		int ret;
		char tmp[4096] = {0};	// 临时用于存放数据的缓冲区
		ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&addr, &len);
		if(ret < 0){
			perror("recvfrom error");
			return -1;
		}
		buf->assign(tmp, ret);	// 给buf申请ret大小的空间,从tmp中拷贝ret长度的数据进去
		// 为了接口灵活,用户若不想获取地址信息,则不再转换获取
		// 只有当用户想要获取地址的时候,这时候传入缓冲区,我们将数据写入进去
		if(ip != NULL){
			*ip = inet_ntoa(addr.sin_addr);	// 将网络字节序整数IP地址转换为字符串地址,返回
		}
		if(port != NULL){
			*port = ntohs(addr.sin_port);
		}
		return true;
	}
	// 5.关闭套接字
	void Close(){
		close(_sockfd);
		_sockfd = -1;
		return ;
	}
private:
	// 贯穿全文的套接字描述符
	int _sockfd;
	
};

#define CHECK_RET(q) if((q) == false){ return -1; }

// 客户端要给服务端发送数据,那么就需要知道服务端的地址信息
// 因此通过程序运行参数传入服务端的地址信息
int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em: ./udp_cli 192.168.122.132 9000\n");
		return -1;
	}
	// argv[0] = ./udp_cli;
	// argv[1] = 192.168.122.132
	// argv[2] = 9000
	std::string ip_addr = argv[1];	// 服务端地址信息
	uint16_t port_addr = atoi(argv[2]);

	UdpSocket sock;
	CHECK_RET(sock.Socket());	// 创建套接字
	//CHECK_RET(sock.Bind());	// 绑定地址信息
	while(1){
		// 获取一个标准输入的数据,进行发送
		std::cout << "client say: ";
		fflush(stdout);
		std::string buf;
		std::cin >> buf;	// 获取标准输入的数据
		sock.Send(buf, ip_addr, port_addr);	// 向指定的主机进程发送buf数据
		buf.clear();	// 清空buf缓冲区
		sock.Recv(&buf);// 因为本身客户端就知道服务端的地址,因此不需要再获取了
		std::cout << "server say: " << buf << std::endl;
	}
	sock.Close();

	return 0;
}
  • udp服务端程序

使用C语言编写

代码示例:

// 编写一个udp服务端的C语言程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>	// sockaddr结构体/IPPROTO_UDP
#include <arpa/inet.h>	// 包含一些字节序转换的接口
#include <sys/socket.h>	// 套接字接口头文件

int main(int argc, char *argv[]){
	// argc表示参数个数,通过argv向程序传递端口参数
	if(argc != 3){
		printf("./udp_srv ip port  em: ./udp_srv 192.168.122.132 3000\n");
		return -1;
	}
	const char * ip_addr = argv[1];
	uint16_t port_addr = atoi(argv[2]);

	// socket(地址域,套接字类型,协议类型)
	int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if(sockfd < 0){
		perror("socket error");
		return -1;
	}
	// bind(套接字描述符, 地址结构,地址长度)
	// struct sockaddr_in ipv4地址结构
	// struct in_addr{ uint32_t s_addr }
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	// htons - 将两个字节的主机字节序整数转换为网络字节序的整数
	addr.sin_port = htons(port_addr);	// 注意千万不要使用htonl(端口是2个字节大小)
	// inet_addr 将一个点分十进制的字符串IP地址转换为网络字节序的整数IP地址
	addr.sin_addr.s_addr = inet_addr(ip_addr);
	socklen_t len = sizeof(struct sockaddr_in);	// 获取IPv4地址结构长度
	int ret = bind(sockfd, (struct sockaddr*)&addr, len);
	if(ret < 0){
		perror("bind error");
		return -1;
	}
	
	while(1){
		char buf[1024] = { 0 };
		struct sockaddr_in cli_addr;
		socklen_t len = sizeof(struct sockaddr_in);
		// recvfrom(描述符,缓冲区,长度,参数,客户端地址信息,地址信息长度)
		// 阻塞接收数据,将数据放入buf中,将发送端的地址放入cliaddr中
		int ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cli_addr, &len);
		if(ret < 0){
			perror("recfrom error");
			close(sockfd);	// 关闭套接字
			return -1;
		}
		printf("client say: %s\n", buf);

		memset(buf, 0x00, 1024);	// 清空buf中的数据
		printf("server say: ");
		fflush(stdout);	// 用户输入数据,发送给客户端
		scanf("%s", buf);
		// 通过sockfd将buf中的数据发送到cli_addr客户端
		ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cli_addr, len);
		if(ret < 0){
			perror("sendto error");
			close(sockfd);	// 关闭套接字
			return -1;
		}
	}
	close(sockfd);	// 关闭套接字
}

在这里插入图片描述

客户端程序中流程的特殊之处:

客户端通常不推荐用户自己绑定地址信息,而是让操作系统在发送数据的时候发现socket还没有绑定地址,然后自动选择一个合适的ip地址和端口进行绑定。

  1. 如果不主动绑定,操作系统会选择一个合适的地址信息进行绑定(什么地址就是合适的地址? — 当前没有被使用的端口
  • 一个端口只能被一个进程占用,若用户自己指定端口以及地址进行绑定有可能这个端口已经被使用了,则会绑定失败

  • 让操作系统选择合适的端口信息,可以尽最大能力避免端口冲突的概率

  • 对于客户端来说,不关心是用什么源端地址将数据发送出去,只要能够发送数据并且接受数据就可以

  1. 服务端可不可以也不主动绑定地址? — 不可以的
  • 客户端所知道的服务端的地址信息,都是服务器告诉客户端的
  • 一旦服务端不主动绑定地址,则会造成操作系统随意选择合适的地址进行绑定,服务端自己都不确定自己用了什么地址信息,
  • 如何告诉客户端? — 服务端通常必须主动绑定地址,并且不能随意改动

每个程序绑定的都是自己的网卡地址信息
客户端发送的对端地址信息一定是服务端绑定的地址信息
服务端绑定的地址信息,一定是自己主机上的网卡IP地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章