socket编程

socket编程(三):

网络通信

Internet domain 流 socket 是基于 TCP 之上的,它们提供了可靠的双向字节流通信信道。

Internet domain 数据报 socket 是基于 UDP 之上的。UDP socket 与 UNIX domain 中的
数据报类似,但需要注意下列差别。

  • UNIX domain 数据报 socket 是可靠的, 但 UDP socket 则是不可靠的—数据报可能会丢失、重复或到达的顺序与它们被发送的顺序不同。
  • 在一个 UNIX domain 数据报 socket 上发送数据会在接收 socket 的数据队列为满时阻塞。与之不同的是,使用 UDP 时如果进入的数据报会使接收者的队列溢出,那么数据报就会静默地被丢弃。

1、网络字节序

网络字节序是大端字节序, 有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据。
如何证明自己的机器采用了哪种字节顺序:

#include <stdio.h>

int check()
{
	int i = 1; //1在内存中的表示: 0x00000001
	char *pi = (char *)&i; //将int型的地址强制转换为char型
	return *pi == 0; //如果读取到的第一个字节为1,则为小端法,为0,则为大端法
}

int main()
{
	if (check() == 1)
		printf("big\n");
	else
		printf("little\n");

	return 0;
}

htonl()、 htons()、ntohl()以及 ntohs()函数被定义(通常为宏)用来在主机和网络字节序之间转换整数。

#include <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);

2、Internet socket 地址

2.1 IPv4 socket 地址: struct sockaddr_in

一个 IPv4 socket 地址会被存储在一个sockaddr_in 结构中,该结构在<netinet/in.h>中进行定义,具体如下。

struct in_addr{                /* IPv4 4-byte address */
    in_addr_t s_addr;          /* Unsigned 32-bit integer */
}

struct sockaddr_in {           /* IPv4 socket address */
    sa_family_t	sin_family;    /* Address family(AF_INET) */
	in_port_t	sin_port;      /* Port number */
	struct in_addr	sin_addr;  /* IPv4 address */
	__int8_t	sin_zero[8];   /* Same size as struct sockaddr */
}

sockaddr_in 结构中的 sin_family 字段,其值总为 AF_INET。 sin_portsin_addr 字段是端
口号和 IP 地址,它们都是网络字节序的。 in_port_tin_addr_t 数据类型是无符号整型,其长度分别为 16 位和 32 位。

2.2 IPv6 socket 地址: struct sockaddr_in6

struct in6_addr {
	union {
		uint8_t		__u6_addr8[16];
		uint16_t	__u6_addr16[8];
		uint32_t	__u6_addr32[4];
	} __u6_addr;			/* 128-bit IP6 address */
};

struct sockaddr_in6 {           /* IPv6 socket address */
	sa_family_t	sin6_family;	/* AF_INET6 */
	in_port_t	sin6_port;	    /* Port number*/
	uint32_t	sin6_flowinfo;	/* IPv6 flow information */
	struct in6_addr	sin6_addr;	/* IPv6 address */
	uint32_t	sin6_scope_id;	/* Scope ID */
};

in6_addr 结构包含了一个 union 定义将 128 位的 IPv6 地址划分成 16 字节或八个 2 字节的整数或四个 32 字节的整数。

sin_family 字段会被设置成 AF_INET6。 sin6_portsin6_addr 字段分别是端口号和 IP。剩余的字段 sin6_flowinfosin6_scope_id 一般会将它们设置为 0。 sockaddr_in6 结构中的所有字段都是以网络字节序存储的。

2.2.1 IPv6通配地址

IPv6 和 IPv4 一样也有通配和回环地址,但它们的用法要更加复杂一些,因为 IPv6 地址是存储在数组中的(并没有使用标量类型),下面将会使用 IPv6 通配地址(0::0)来说明这一点。系统定义了常量 IN6ADDR_ANY_INIT 来表示这个地址,具体如下:

#define IN6ADDR_ANY_INIT {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}

在变量声明的初始化器中可以使用IN6ADDR_ANY_INIT 常量,但无法在一个赋值语句的右边使用这个常量,因为 C 语法并不允许在赋值语句中使用一个结构化的常量。取而代之
的做法是必须要使用一个预先定义的变量in6addr_any, C 库会按照下面的方式对该变量进行初始化。

const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT;

因此可以像下面这样使用通配地址来初始化一个 IPv6 socket 地址:

struct sockaddr_in6 addr;

memset(&addr, 0, sizeof(struct sockaddr_in6));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons(PORT_NUM);
2.2.2 IPv6回环地址

IPv6 环回地址( ::1)的对应常量和变量是 IN6ADDR_LOOPBACK_INIT 和 in6addr_loopback。

#define IN6ADDR_LOOPBACK_INIT {{{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }}}

IPv6 的常量和变量初始化器是网络字节序的,必须要确保端口号是网络字节序的。

如果 IPv4 和 IPv6 共存于一台主机上,那么它们将共享同一个端口号空间。这意味着如果一个应用程序将一个 IPv6 socket 绑定到了 TCP 端口 2000 上(使用 IPv6 通配地址),那么 IPv4 TCP socket 将无法绑定到同一个端口上。 ( TCP/IP 实现确保位于其他主机上的socket 能够与这个 socket 进行通信,不管那些主机运行的是 IPv4 还是 IPv6。)

2.3 sockaddr_storage 结构

这个结构的空间足以存储任意类型的 socket 地址(即可以将任意类型的 socket 地址结构强制转换并存储在这个结构中)。特别地,这个结构允许透明地存储 IPv4 或 IPv6 socket 地址,从而删除了代码中的 IP 版本依赖性。

 struct sockaddr_storage
 {
     sa_family_t ss_family;           /* Address family */
     __ss_aligntype __ss_align;       /* Force alignment.  */
     char __ss_padding[_SS_PADSIZE];  /*Pad to 128 bytes*/
};

3、inet_pton()和 inet_ntop()函数

inet_pton()和 inet_ntop()函数允许在 IPv4 和 IPv6 地址的二进制形式和点分十进制表示法或十六进制字符串表示法之间进行转换。

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

inet_pton()函数将 src 中包含的字符串转换成网络字节序的二进制 IP 地址。af参数应该被指定为 AF_INET 或 AF_INET6。转换得到的地址会被放在 dst 指向的结构中,它应该根据在 af 参数中指定的值指向一个 in_addr 或 in6_addr 结构。

inet_ntop()函数执行逆向转换。 同样, af 应该被指定为 AF_INET 或AF_INET6,src应该指向一个待转换的 in_addr 或 in6_addr 结构。得到的以 null 结尾的字符串会被放置在 dst 指向的缓冲器中。 size 参数必须被指定为这个缓冲器的大小。 inet_ntop()在成功时会返回 dst。如果 size的值太小了,那么inet_ntop()会返回 NULL 并将 errno设置成 ENOSPC。

正确计算 dst 指向的缓冲器的大小可以使用在<netinet/in.h>中定义的两个常量。 这些常量标识出了 IPv4 和 IPv6 地址的展现字符串的最大长度(包括结尾的 null 字节)。

#define	INET_ADDRSTRLEN		16
#define	INET6_ADDRSTRLEN    46

4、服务器/客户端示例

服务端示例:

#include <stdio.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <net/if.h>

#define SERV_PORT 6666
#define BACKLOG   10
#define QUIT      "quit"

int main(int argc, char *argv[])
{
	//01、创建一个用于接待的套接字
	int fd = -1;
	//创建流式套接字SOCK_STREAM,使用AF_INET(IPv4),使用默认协议(0)
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd<0){
		perror("socket");
		exit(1);
	}

	//为了防止异常终止服务器后,不能快速启动服务器绑定地址
	//我们设置允许绑定地址快速重用
	int b_reuse = 1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR ,&b_reuse,sizeof(int));
	//把fd设置成允许地址快速重用
	//SOL_SOCKET在应用层
	//SO_REUSEADDR选择这个选项是快速重用选项
	//b_reuse是要设置的数值
	//因为设置大数值,可能类型不一样,
	//所以setsockopt要求传入地址的长度为了百搭

	//02、绑定信息
	//先要填充地址信息的结构体,才能绑定到fd
	//创建ipv4的地址信息结构体
	struct sockaddr_in sin;
	bzero(&sin,sizeof(sin));
	sin.sin_family     = AF_INET;//选择IPv4
	sin.sin_port       = htons(SERV_PORT);
	//填充一个网络字节序大端口号,这里使用htons把宏SERV_PORT转换
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	//填充服务器要绑定大ip地址,
	//这里为了泛指本机所有网卡(网卡ip地址)
	//使用“0.0.0.0”,或宏INADDR_ANY,他本质就是0所以使用htonl进行转换
	//此时填充结构体ok
	//那就把fd绑定到这个地址上
	if(bind(fd,(struct sockaddr*)&sin,sizeof(sin))<0){
		perror("bind");
		exit(1);
	}//把刚才填充好了大地址信息和fd进行绑定
	//方便客户端能找到服务器

	//03、调用listen()把主动套接字变成被动套接字
	if(listen(fd,BACKLOG)<0){
		perror("listen");
		exit(1);
	}
	//把fd套接字变成被动套接字,
	//并设置最大能改接受BACKLOG个客户端

	//04、阻塞等待客户端发起连接请求
	int newfd = -1;
	struct sockaddr_in cin;
	//定义一个空的地址信息结构体,用来成放客户端的地址信息
	socklen_t addrlen = sizeof(cin);

	while(1){
		//定一个地址长度变量
		newfd = accept(fd,(struct sockaddr*)&cin,&addrlen);
		if(newfd<0){
			perror("accept");
			exit(1);
		}

		//通过fd的监听,accept接受客户端连接,
		//并把建立好连接大客户端地址信息填充到cin这个结构体中
		char ipv4_addr[16];
		if(!inet_ntop(AF_INET,(void*)&cin.sin_addr.s_addr,ipv4_addr,sizeof(cin))){
			perror("inet_ntop");//使用ntop把客户端大ip地址转换并存放到ipv4_addr数组中
			exit(1);
		}
		printf("++客户端(%s:%d)已经连接成功!",ipv4_addr,ntohs(cin.sin_port));
		//打印已经连接了的客户端信息

		//05读写(收发)消息
		//这里的读写是从newfd
		int ret =-1;
		char buf[BUFSIZ];
		//这里的BUFSIZ是stdio中给我们定义好了大一个宏,他是8192
		while(1){
			//因为要循环读且内容存放在buf数组里
			//有必要清零操作
			bzero(buf,BUFSIZ);
			ret = read(newfd,buf,BUFSIZ-1);
			if(ret<0){
				perror("read");
				break;
			}
			if(ret==0){//如果收到大是0说明对方已经关闭了
				break;
			}
			printf("收到:%s\n",buf);
			if(!strncasecmp(buf,QUIT,strlen(QUIT))){
				//判断对方是否输入并发送给服务器quit退出
				printf("客户端已经退出!\n");
				break;
			}
		}
		close(newfd);
		//如果newfd使用完了,必须关闭newfd,负责内存泄漏
	}
	close(fd);

	return 0;
}

客户端示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
    
#define SERV_PORT  6666
//自定义一个端口号的宏
#define SERV_IP_ADDR  "192.168.247.131"
#define QUIT "quit"
//定义一个退出的宏


int main(int argc, char *argv[])
{
	//创建套接字
	int fd =-1;
	fd=socket(AF_INET,SOCK_STREAM,0);
	//创建用于通讯的流式套接字,SOCK_STREAM
	if(fd < 0){//判断套接字是否创建成功
		perror("socket");
		return -1;
	}
	puts("***创建套接字成功");
	
	//填充想要连接的服务器地址信息
	struct sockaddr_in sin;//创建服务器的结构体
	bzero(&sin,sizeof(sin));//可以给结构体清个零
	sin.sin_family     = AF_INET;//选择地址族(IPV4的)
	sin.sin_port       = htons(SERV_PORT);//设置端口号
	//这里端口号需要网络字节序,所以使用htons进行转换
	sin.sin_addr.s_addr= inet_addr(SERV_IP_ADDR);
	//这里从键盘读取服务器的ip地址
	//并且使用inet_addr进行转换为网络字节序四字节整数
	//目标服务器的信息填充完毕
	puts("***填充对方服务器地址信息完成");
	
	//使用connect发起连接请求
	if( connect(fd,(struct sockaddr*)&sin,sizeof(sin)) <0){
		//通过fd向sin结构体内所存地址信息发起连接请求
		perror("connect");
		exit(1);
	}
	//能够运行到这一行说明tcp连接成功
	//可以进行tcp连接通讯

	puts("***客户端就绪");

	char buf[BUFSIZ];
	//自己定义一个存放数据的buf作为缓冲区
	int ret = -1;
	//定义变量存放发送函数的返回值
	while(1){
		//从键盘输入内容存放到buf里
		//每次循环都使用他有必要给他清零
		bzero(buf,BUFSIZ);
		if(fgets(buf,BUFSIZ-1,stdin)==NULL){
			//从键盘获取输入存放到buf数组中
			perror("fgets");
			continue;
			//如果获取出错重新获取
		}
		
		//发送数据
		ret = write(fd,buf,strlen(buf));
		//因为tcp是有链接的我知道对方是谁
		//又因为fd是特殊的文件描述符
		//这里可以使用write进行发送消息
		if(ret<0){
			perror("send");
			exit(1);
		}
		if(!ret){//说明对方关闭了
			break;
		}

		//判断是不是向退出
		if(!strncasecmp(buf,QUIT,4)){
			//使用strncasecmp进行比较输入是否是quit
			//而且比较4个字节,不区分大小写进行比较
			puts("***我已退出");
			break;//如果输入的quit,那就退出
		}
	}
	//通讯完成可以关闭fd回收资源
	close(fd);	
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值