16_网络IPC11-流式套接字

C端
1 获取socket socket()
2 给socket取得地址(可省略) bind() 绑定IP地址和端口
3 发送连接 connect()
4 收发消息 recv()
5 关闭连接 close()

S端
1 获取socket socket()
2 给socket取得地址 bind() 绑定IP地址和端口
3 将socket置为监听模式 lsiten()
4 接受连接 accept()
5 收发消息 send()
6 关闭连接 close()

listen() 监听一个SOCKET

NAME
       listen - listen for connections on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

backlog原本是指 半连接池的大小,原本是指可以存放多少半连接状态的节点。但是由于因为半连接洪水的攻击,已经放弃了半连接池,所以 backlog 现在变成了指 S端能够承受建立全连接的C端的最大数量。

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

accept() 在SOCKET上建立连接

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

/*
struct sockaddr *addr: 对端地址信息
socklen_t *addrlen: 对端地址长度
*/
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
同样

 struct sockaddr 依赖于当前用到的协议族中的地址信息 The sockaddr structure is defined as something like:

       struct sockaddr {
           sa_family_t sa_family;
           char        sa_data[14];
       }

不同的协议族 来 绑定自己这端的地址 所用的结构体是不一样的。所以是不存在  struct sockaddr 类型的。所以我们的处理方式是:我们用的是哪一个协议族,就把该协议族地址作为addr ,然后再把地址长度写到addrlen 

 AF_INET see ip(7)

man 7 ip

 Address format
       An IP socket address is defined as a combination of an IP interface address and a 16-bit port number.  The basic IP protocol does not supply port numbers, they are implemented by higher level
       protocols like udp(7) and tcp(7).  On raw sockets sin_port is set to the IP protocol.

/*
注意 :IP地址和端口,是需跟着网络一起发送的。代表自己的身份
*/
           struct sockaddr_in {
//协议族 address family: AF_INET
               sa_family_t    sin_family; 

 	       //需要的端口
               in_port_t      sin_port;  

//IP地址 并非点分式,而是大整数internet address ,用的时候需要格式转换:inet_pton()
               struct in_addr sin_addr;   
           };

           /* Internet address. */
           struct in_addr {
               uint32_t       s_addr;     /* address in network byte order */
           };


所以 AF_INET 协议族中的 协议地址类型为 struct sockaddr_in 


RETURN VALUE
       如果成功,这些系统调用将返回一个非负整数,它是所接受套接字的描述符。出现错误时,返回-1,并适当地设置errno。

实验:流式套接字

proto.h

#ifndef PROTO_H_
#define PROTO_H_

#define SERVERPORT "1989"
#define FMT_STAMP "%lld\r\n"
	
#endif

server.c

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

#define IPSTRSIZE 40
#define BUFSIZE 1024


static void server_job(int sd)
{
	char buf[BUFSIZE];
	int len;
	
//将时间戳 以 FMT_STAMP格式 存储到 buf中
	len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
	
	if(send(sd,buf,len,0) < 0)
	{
		perror("send()");
		exit(1);
	}
}


int main()
{
	int sd,newsd;

	struct sockaddr_in laddr,raddr;
	socklen_t raddr_len;
	char ipstr[IPSTRSIZE];

//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
	sd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);
	if(sd < 0)
	{
		perror("soccket()");
		exit(1);
	}

 //设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in 

	laddr.sin_family = AF_INET; // 协议族为 AF_INET 
 // 设置端口为1989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons
	laddr.sin_port = htons(atoi(SERVERPORT));
 // 设置IP 
	inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);//0.0.0.0:any address

// 给SOCKET绑定一个地址,关联到目标协议族地址信息结构体,即绑定IP,端口
//(void *)&laddr, 实际并没有struct sockaddr类型,所以暂时强转为 void *
	if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
	{
		perror("bind");
		exit(1);
	}

//将socket置为监听模式, 承受C端最大数量为200
	if(listen(sd,200) < 0)
	{
		perror("listen");
		exit(1);	
	}

	/* !!!! */
	raddr_len = sizeof(raddr);

	while(1)
	{
  //接受连接,保存对端地址信息,如果成功,将返回一个非负整数,它是所接受套接字的描述符,即newsd 
  //默认为阻塞等待连接
		newsd = accept(sd,(void *)&raddr,&raddr_len);
		if(newsd < 0)
		{
			perror("accept");
			exit(1);	
		}
		
 //将接收到的对端地址中IP信息 从大整数 转换为 点分式 保存到ipstr数组
		inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);

//打印对端 IP:端口
//raddr.sin_port  是从socket接收到的信息,不是单字节信息,需要字节序转换 ntohs
		printf("Client %s:%d\n",ipstr,ntohs(raddr.sin_port));


		server_job(newsd);
		close(newsd);

	}	

	close(sd);

	exit(0);

}

在这里插入图片描述
可以看到 1989端口已经打开,目前处于 Listen模式。

用标准客户端nc 工具自测,即 nc ip port,即
nc 127.0.0.1 1989 :请求向 127.0.0.1机器(即本机)1989端口 发送请求,可以看到有数据返回,当前时间戳

在这里插入图片描述一个关键问题:

在 CTL+C 结束刚刚运行的 server端进程后,立刻再次运行 server端进程,会提示
Address already in use

再次 netstat -ant 后会显示 1989端口 处于 TIME_WAIT状态,表明该端口还在工作当中。问题就在于 之前 CTL+C 结束进程,用信号将进程终止,但是server端 最后的 close(sd) 并没有执行到,也就是没有正常结束,没有去刷新该刷新的流,也没有将当前的SOCKET 和 端口释放,所以紧随其后的再次运行 server进程会提示 端口busy,就是因为上一次结束没有正常释放端口, 而等一会儿就可以继续申请1989端口,这是因为内核发现了 1989端口 对应的 socket已经被异常终止了,内核就会帮忙释放掉 1989端口。这样就可以再次运行server服务了。

解决端口释放问题:

添加属性 : SO_REUSEADDR,表明再次 bind()的时候,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。

对指定Socket sd 的SOL_SOCKET层面 的 SO_REUSEADDR属性进行设置,即打开该属性,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。

int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,&val, sizeof(val)) < 0)
{
	perror("setsockopt()");
	exit(1);
}

所以server端最终代码应该加上 SO_REUSEADDR 属性

server.c

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

#define IPSTRSIZE 40
#define BUFSIZE 1024


static void server_job(int sd)
{
	char buf[BUFSIZE];
	int len;
	
//将时间戳 以 FMT_STAMP格式 存储到 buf中
	len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
	
	if(send(sd,buf,len,0) < 0)
	{
		perror("send()");
		exit(1);
	}
}


int main()
{
	int sd,newsd;

	struct sockaddr_in laddr,raddr;
	socklen_t raddr_len;
	char ipstr[IPSTRSIZE];

//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
	sd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);
	if(sd < 0)
	{
		perror("soccket()");
		exit(1);
	}


//对指定Socket sd 的SOL_SOCKET层面 的 SO_REUSEADDR属性进行设置,即打开该属性,如果发现 端口没有释放,该属性会马上释放该端口,并且让我们绑定成功。
	int val = 1;
	if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR,&val, sizeof(val)) < 0)
	{
		perror("setsockopt()");
		exit(1);
	}

 //设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in 

	laddr.sin_family = AF_INET; // 协议族为 AF_INET 
 // 设置端口为1989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons
	laddr.sin_port = htons(atoi(SERVERPORT));
 // 设置IP 
	inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);//0.0.0.0:any address

// 给SOCKET绑定一个地址,关联到目标协议族地址信息结构体,即绑定IP,端口
//(void *)&laddr, 实际并没有struct sockaddr类型,所以暂时强转为 void *
	if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
	{
		perror("bind");
		exit(1);
	}

//将socket置为监听模式, 承受C端最大数量为200
	if(listen(sd,200) < 0)
	{
		perror("listen");
		exit(1);	
	}

	/* !!!! */
	raddr_len = sizeof(raddr);

	while(1)
	{
   //接受连接,保存对端地址信息,如果成功,将返回一个非负整数,它是所接受套接字的描述符,即newsd 
  //默认为阻塞等待连接
		newsd = accept(sd,(void *)&raddr,&raddr_len);
		if(newsd < 0)
		{
			perror("accept");
			exit(1);	
		}
		
 //将接收到的对端地址中IP信息 从大整数 转换为 点分式 保存到ipstr数组
		inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);

//打印对端 IP:端口
//raddr.sin_port  是从socket接收到的信息,不是单字节信息,需要字节序转换 ntohs
		printf("Client %s:%d\n",ipstr,ntohs(raddr.sin_port));


		server_job(newsd);
		close(newsd);

	}	

	close(sd);

	exit(0);

}

客户端:

connect() 建立连接 socket

NAME
	       connect - initiate a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

/*
 const struct sockaddr *addr:对端地址,同样,没有这样类型,我们此时使用的地址是struct sockaddr_in类型
socklen_t addrlen:对端地址长度
*/
       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

RETURN VALUE
       If the connection or binding succeeds, zero is returned.  On error, -1 is returned, and errno is set appropriately.

client.c

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

int main(int argc,char *argv[])
{
	int sd;
	struct sockaddr_in raddr;
	long long stamp;
	FILE *fp;

	if(argc < 2)
	{
		fprintf(stderr,"Usage...!\n");
		exit(1);
	}

//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
	sd = socket(AF_INET,SOCK_STREAM,0);
	if(sd < 0)
	{
		perror("soccket()");
		exit(1);
	}

	//bind()
	
//设置对端地址信息,即设置S端地址信息
	raddr.sin_family = AF_INET;
	raddr.sin_port = htons(atoi(SERVERPORT));
	inet_pton(AF_INET,argv[1],&raddr.sin_addr);

//建立连接
	if(connect(sd,(void *)&raddr,sizeof(raddr)) < 0)
	{
		perror("connect()");
		exit(1);
	}

/*
接下来就是接收数据,正常使用 recv()函数接收,这里用文件操作来解析接收的数据来加深 一切皆文件的思想,本身sd就是一个文件描述符
将给定文件描述符 并指定操作权限,转换为 FILE* 文件流的操作,转换成功之后,我们对当前socket的操作就完全转换为了标准IO 的操作,
这样所有可移植的标准库函数都可以使用,fread()  fwrite() ...
*/
	fp = fdopen(sd,"r+");
	if(fp == NULL)
	{
		perror("fdopen()");
		exit(1);
	}

//从指定流fp中拿数据 以 format格式存储到 目标地址
	if(fscanf(fp,FMT_STAMP,&stamp)<1)
		fprintf(stderr,"Bad format!\n");
	else
		fprintf(stdout,"stamp = %lld\n", stamp);

//关闭fp
	fclose(fp);

	exit(0);

}

C端请求 127.0.0.1 上面的内容,成功返回当前时间戳。

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux老A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值