Socket套接字通信 TCP UDP详解(网络通信)

文章目录

    • 一 什么是套接字Socket
      • 1.Socket简介
      • 2.Socket的域(domain)
      • 3.Socket主要类型(type)
      • 4.Socket基本工作流程
    • 二 创建套接字Socket
      • 1.socket函数
    • 三 绑定套接字Socket与主机网络地址
      • 1.bind函数
      • 2.struct sockaddr与struct sockaddr_in
      • 3.常用填充地址信息的方法
      • 4.主机字节序与网络字节序
    • 四 UDP通信的实现
      • 1.recvfrom函数
      • 2.sendto函数
      • 3.示例
    • 五 TCP通信的实现
      • 1.listen函数(server端)
      • 2.accept函数(server端)
      • 3.connect函数(client端)
      • 4.write与read函数
      • 5.send与recv函数
      • 6.示例
    • 六 套接字的缓冲区以及阻塞模式
      • 1.缓冲区
      • 2.使用write/send发送数据
      • 3.使用read/recv读取数据
    • 七 总结套接字收发数据的过程

一 什么是套接字Socket

1.Socket简介

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口 。
Socket套接字通信 TCP UDP详解(网络通信)_第1张图片

Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制 。

2.Socket的域(domain)

域指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 网络,许多 Linux 局域网使用的都是该网络,当然,因特网自身用的也是它。

3.Socket主要类型(type)

  1. 流套接字(SOCK_STREAM)
    流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。
  2. 数据报套接字(SOCK_DGRAM)
    数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
  3. 原始套接字(SOCK_RAW)
    原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。

4.Socket基本工作流程

要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :

  1. 服务器监听
    所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。
  2. 客户端请求
    所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求 。
  3. 连接确认
    所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求 。

二 创建套接字Socket

1.socket函数

int socket(int domain, int type, int protocol);
/*
1.函数功能:创建套接字
2.参数:
	int domain:套接字的域通常为 AF_INET(IPv4)或者AF_INET6(IPV6)
	int type:套接字类型通常为 SOCK_STREAM、SOCK_DGRAM
	int protocol:
							 0 :使用默认协议
					IPPROTO_TCP:使用TCP协议
					IPPROTO_UDP:使用UDP协议
3.返回值:
		成功:返回套接字描述符
		失败:-1
*/

三 绑定套接字Socket与主机网络地址

1.bind函数

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
1.函数功能:绑定套接字Socket与主机网络地址信息
2.参数:
	int sockfd:				 套接字描述符
	const struct sockaddr *addr:主机地址信息,下文详解
	socklen_t addrlen:			 参数2的长度(字节)
3.返回值:
		成功:0
		失败:-1
*/

2.struct sockaddr与struct sockaddr_in

//以下主要摘自LINUX手册
typedef unsigned short int sa_family_t;
/* Structure describing a generic socket address.翻译:描述通用套接字地址的结构  */
struct sockaddr {    
          			sa_family_t sa_family;//地址族
 					char    sa_data[14];//14字节,包含套接字中的目标地址和端口信息  
				}

/* Structure describing an Internet socket address.翻译:描述Internet套接字地址的结构  */
struct sockaddr_in {
						sa_family_t    sin_family; /* address family: AF_INET */
               			in_port_t      sin_port;   /* port in network byte order */
               			struct in_addr sin_addr;   /* internet address */
    					char		   sin_zero[8];//占位不使用,用来与struct sockaddr对齐
					};
/* Internet address */
struct in_addr  {
			/*uint32_t*/ in_addr_t  s_addr;/* address in network byte order地址的网络字节序 */
				};

/*sin_addr  is  the  IP host address. 
 The s_addr member of struct in_addr contains the host interface address in network byte order. 
 翻译:sin_addr为主机IP地址。struct in_addr的s_addr成员以网络字节顺序包含主机接口地址*/
  1. 这两个结构体一样大,都是16个字节,而且都有family属性,二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。不同的是:
    sockaddr结构体中sa_data成员融合了端口与地址信息,而sockaddr_in结构体用两个成员sin_portsin_addr分别表示端口号和地址信息
  2. sin_port和sin_addr都必须是网络字节序(NBO Network byte order),一般可视化的数字都是主机字节序(HBO Host byte order),下文详解
  3. sockaddr是给操作系统用的。程序员应使用sockaddr_in来表示地址,把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数。sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

示例

int sockfd;
struct sockaddr_in serverAddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* 填充struct sockaddr_in */
bzero(&serverAddr, sizeof(serverAddr));//初始化为0状态 主要是对成员sin_zero[8]清0
serverAddr.sin_family = AF_INET; //设置地址家族
serverAddr.sin_port = htons(SERV_PORT);//端口号1024-65535
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* 强制转换成struct sockaddr */
bind(sockfd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));

3.常用填充地址信息的方法

//填充IP地址
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0.0 等号后面可以是htonl(0)或者0
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&serverAddr.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

//填充端口
serverAddr.sin_port = htons(1234);//端口号1024-65535
serverAddr.sin_port = htons(0);//随机端口 等号后面可以 0

相关函数:
1. inet_addr

in_addr_t inet_addr(const char *cp);
/*
功能:点分字符串格式地址转网络格式
参数:IPv4地址字符串例如"127.0.0.1"
返回值:
		成功:返回网络字节序的地址用于赋值serverAddr.sin_addr.s_addr
		失败:-1
*/

2.inet_ntoa 、inet_aton

char *inet_ntoa (struct in_addr in) //net to ascii
/*
功能:网络字节序地址转点分字符串格式地址
参数:传入通用的网络字节序地址struct in_addr sin_addr
返回值:
		成功:返回指针指向IPv4点分字符串格式地址 例如"127.0.0.1"
		失败:0
*/
int inet_aton(const char *cp, struct in_addr *inp); //ascii to net
/*
功能:点分字符串格式地址转网络格式地址
参数:
		 cp:IPv4点分字符串格式地址
		inp:网络字节序地址struct in_addr sin_addr
返回值:
		成功:非0
		失败:0
*/

4.htons、htonl

uint16_t htons(uint16_t hostshort);//h host n net s short
uint32_t htonl(uint32_t hostlong);//h host n net l long
/*
功能:将主机字节序的short/long类型数据转为网络字节序类型数据
参数:
		short类型数据/long类型
返回值:
		成功:网络字节序类型数据
		失败:-1
*/

5.inet_ptoninet_ntop
这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。

int inet_pton(int family, const char *strptr, void *addrptr);
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//返回值:若成功则为指向结构的指针,若出错则为NULL

4.主机字节序与网络字节序

NBO : 网络字节序
HBO : 主机字节序
LE little-endian:小端
BE big-endian:大端

  1. 网络字节序和主机字节序:
    网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。
    发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。
    TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。

tcp/ip规定它们的网络字节序都是大端字节序。主机字节序可能是大端也可能是小端,与主机的cpu有关,与操作系统无关考虑到与协议的一致以及与同类其它平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字 节序转换为主机字节序。网络程序开发时 或是跨平台开发时 应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。数据在传输的过程中,一定有一个标准化的过程,也就是说:
从主机a到主机b进行通信:a的主机字节序——网络字节序——b的主机字节序

  1. 大端字节序和小端字节序:
    大端字节序存储时值的高位存储在较小的地址,值的低位存储在较大的地址。
    小端字节序存储时值的高位存储在较大的地址,值的低位存储在较小的地址。
    以0x12345678为例:
    地址:0x1000  0x1001  0x1002  0x1003
    小端: 78    56    34    12
    大端: 12    34    56    78
  2. 测试主机是大端还是小端的方法:
int main()
{
	union
	{
		short s;
		char c[sizeof(short)];
	}un;
	un.s = 0x0102;
    if(sizeof(short)==2)
    {
    	if(un.c[0] == 1 && un.c[1] == 2)
			printf("Big-Endian\n");
		else if(un.c[0] == 2 && un.c[1] == 1)
			printf("Little-Endian\n");
		else
			printf("Unknown\n");
	}
	else
		print("sizeof(short)=%d\n",sizeof(short));
	exit(0);
}

四 UDP通信的实现

在创建并绑定套接字之后,我们就可以尝试TCP、UDP通信了。
TCP/IP协议是一个协议簇。里面包括很多协议,UDP只是其中的一个。

  • UDP(User Datagram Protocol用户数据报协议)是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
  • UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。

1.recvfrom函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/*
功能:接收数据
参数:
		int sockfd:socket函数的返回值,套接字描述符
		 void *buf:存放收到的数据
		size_t len:参数2的大小
		 int flags:如果没有数据到来 阻塞等待还是不等待 0表示阻塞 MSG_DONTWAIT 不等待
		 
	   struct sockaddr *src_addr:用于获取发送方的地址信息
	          socklen_t *addrlen:发送方地址信息长度 注意:传的实参必须初始化
返回值:
		成功:返回实际收到的字节数
		失败:-1
*/

2.sendto函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
/*
功能:发送数据给对端
参数:
		 int sockfd:socket函数的返回值,套接字描述符
	const void *buf:要发送的数据存放的地址
		 size_t len:参数2的大小
		  int flags:套接字缓存满 阻塞还是不阻塞 0表示阻塞 MSG_DONTWAIT 不阻塞
		 
	const struct sockaddr *dest_addr:目标端的地址信息
	              socklen_t *addrlen:目标端的地址信息
返回值:
		成功:返回实际发送的字节数
		失败:-1
*/

3.示例

实现服务器端与客户端聊天
运行效果
Socket套接字通信 TCP UDP详解(网络通信)_第2张图片

/***************************/
/*         服务器端         */
/***************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
	char buf_data[1024] = {};
	/*创建套接字*/
        int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	if(sockfd == -1)
	{
		perror("socket");
		exit(1);
	}
	printf("sockfd:%d\n",sockfd);

	/*定义网络地址结构体变量并填充*/
	struct sockaddr_in myselfAddr;
	myselfAddr.sin_family = AF_INET;
	myselfAddr.sin_port = htons(6666);//把短整形转为网络格式
	myselfAddr.sin_addr.s_addr = htonl(INADDR_ANY);//主机格式转网络格式

	/*套接字与主机绑定*/
	int ret_bind = bind(sockfd,(struct sockaddr*)&myselfAddr,sizeof(myselfAddr));
	if(ret_bind == -1)
	{
		perror("bind");
		close(sockfd);
		exit(1);
	}

	/*缓存用于获取对端网络地址信息*/
	struct sockaddr_in buf_sockaddr;
	socklen_t buf_addrlen = sizeof(buf_sockaddr);
		
	printf("等待客户端连接...\n");
	ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
	if(ret_recv == -1)
	{
		perror("recvfrom");
		close(sockfd);
		exit(1);
	}
	
	printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);

	pid_t pid = fork();
	if(pid>0)
	{
		while(1)
		{
			bzero(buf_data,sizeof(buf_data));
			gets(buf_data);
			ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&buf_sockaddr,buf_addrlen);
			if(ret_send == -1)
			{
				perror("sendto");
				close(sockfd);
				exit(1);
			}
			printf("我:%s\n",buf_data);
		}
	}
	else if(pid == 0)
	{
		while(1)
		{
			ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
			if(ret_recv == -1)
			{
				perror("recvfrom");
				close(sockfd);
				exit(1);
			}
			printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
		}
	}
	else
	{
		perror("fork");
		close(sockfd);
		exit(1);
	}
	return 0;
}

/***************************/
/*           客户端        */
/***************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
	char buf_data[1024] = "\0";
	/*创建套接字*/
	int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	if(sockfd == -1)
	{
		perror("socket");
		exit(1);
	}
	printf("sockfd:%d\n",sockfd);

	/*输入并配置对端网络地址信息*/
	short port;
	char IP[20];
	printf("输入对方IP:\n");
	scanf("%s",IP);
	getchar();
	printf("输入对方端口号:\n");
	scanf("%hd",&port);
	getchar();
	
	struct sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(port);//短整型转为网络格式
	serverAddr.sin_addr.s_addr = inet_addr(IP);//字符串格式转网络地址格式

	/*缓存用于获取对端的网络地址信息*/
	struct sockaddr_in buf_sockaddr;
	socklen_t buf_addrlen = sizeof(buf_sockaddr);

	pid_t pid = fork();
	if(pid>0)
	{
		while(1)
		{
			/*发送信息*/
			bzero(buf_data,sizeof(buf_data));
			gets(buf_data);
			ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
			if(ret_send == -1)
			{
				perror("sendto");
				close(sockfd);
				exit(1);
			}		
			printf("我:%s\n",buf_data);
			bzero(buf_data,sizeof(buf_data));
		}
	}
	else if(pid == 0)
	{
		/*接收信息*/
		while(1)
		{
			bzero(buf_data,sizeof(buf_data));
			ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
			if(ret_recv == -1)
			{
				perror("recvfrom");
				close(sockfd);
				exit(1);
			}

			printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
		}
	}
	else
	{
		perror("fork");
		close(sockfd);
		exit(1);
	}
	return 0;
}

五 TCP通信的实现

  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。
  • TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。

1.listen函数(server端)

  • 对于服务器端程序,使用bind 函数绑定套接字后,还需要使用listen 函数让套接字进入被动监听状态,再调用accept 函数,就可以随时响应客户端的请求了。
  • 所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
int listen(int sockfd, int backlog);
/*
功能:使套接字进入被动监听状态
参数:
		 int sockfd: 需要进入监听状态的套接字
		int backlog:请求队列的最大长度
返回值:
		成功:0
		失败:-1
*/
  • 当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为 请求队列(Request Queue)
  • 当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误
  • listen只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept函数

2.accept函数(server端)

  • 当套接字处于监听状态时,可以通过 accept函数来接收客户端请求。
  • listen只是让套接字进入监听状态,并没有真正接收客户端请求,listen后面的代码会继续执行,直到遇到 accept
  • accept 会阻塞程序执行,直到有新的请求到来。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
功能:处理来自客户端的连接请求
参数:
	 		   int sockfd: 处于监听状态的套接字(服务器绑定的套接字也叫监听套接字)
	struct sockaddr *addr:用于获取对端的地址信息
	   socklen_t *addrlen:参数2的大小
返回值:
		成功:返回一个新的套接字(这个套接字与当前发起申请的客户端连接)
		失败:-1
*/

3.connect函数(client端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
功能:客户端发起连接服务器请求
参数:
	 		         int sockfd:client套接字
	const struct sockaddr *addr:对端(服务器)的地址信息
	         socklen_t *addrlen:参数2的大小
返回值:
		成功:0
		失败:-1
*/

4.write与read函数

建立好了 TCP 连接之后,我们就可以把得到的 sockfd 当作文件描述符来使用。

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

需要注意 read 函数的返回值:

  • retval > 0 :实际读到的字节数
  • retval = 0 :
         普通文件 — 到达文件末尾
         管道文件 — 管道写端关闭
         套接字文件 — 对端关闭,网络断开
  • retval < 0 :出错

5.send与recv函数

recv 和 send 函数提供了和 read 和 write 差不多的功能。前3个参数同read、write,第4
个参数用来控制读写操作。

#include 
#include 
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
/*
参数 int flags:
				0:等同于write
	MSG_DONTROUTE:告诉 IP 目的主机在本地网络上面,没有必要查找表,这个标志一般用网络诊断和路由程序里面
		  MSG_OOB:表示可以接收和发送带外的数据
	 MSG_DONTWAIT:仅本操作非阻塞(执行完恢复阻塞模式)
*/
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
/*
参数 int flags:
			    0:等同于read
		 MSG_PEEK:表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容
		  MSG_OOB:表示可以接收和发送带外的数据
	 MSG_DONTWAIT:仅本操作非阻塞(执行完恢复阻塞模式)
	  MSG_WAITALL:表示等到所有的信息到达时才返回。使用这个标志的时候 recv 会一直阻塞,直到指定的条件满足,或者是发生了错误:
			  		1.当读到了指定的字节时,函数正常返回。返回值等于 len
					2.当读到了文件的结尾时,函数正常返回。返回值小于 len
					3.当操作发生错误时,返回 -1,且设置错误为相应的错误号 (errno) 
*/

6.示例

实现客户端与服务器端的对话,服务器端运行时命令行传参端口号,客户端运行时命令行传参服务器的IP和端口号
关于程序中用到的IO多路复用select函数,参考select函数详解

运行效果
Socket套接字通信 TCP UDP详解(网络通信)_第3张图片

/***************************/
/*        服务器端         */
/***************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void error_Handling(char* func,int retval);
void error_of_read(int retval,char* IP);
void sigFun(int sig);
int count = 0;

int main(int argc,char* argv[])
{
	if(argc != 2)
	{
		printf("%s Port",argv[0]);
		exit(1);
	}
	signal(SIGCHLD,sigFun);
	char buf_data[1024] = {};
	/*创建套接字——监听套接字*/
	int listenfd = socket(AF_INET,SOCK_STREAM,0);
	error_Handling("socket",listenfd);

	/*绑定监听套接字与主机网络地址信息*/
	int on = 1;
	int ret_set = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//地址复用
	error_Handling("setsockopt",ret_set);
	struct sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(atoi(argv[1]));
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	int ret_bind = bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
	error_Handling("bind",ret_bind);

	/*设置监听队列的大小*/
	int ret_listen = listen(listenfd,10);
	error_Handling("listen",ret_listen);

	/*监听等待连接*/
	struct sockaddr_in buf_addr;
	socklen_t buf_addrlen = sizeof(buf_addr);
	while(1)
	{
		printf("服务器持续监听中\n");
		printf("连接服务器的客户端数量:%d\n",count++);
		int newconfd = accept(listenfd,(struct sockaddr*)&buf_addr,&buf_addrlen);//返回分机套接字
		error_Handling("accept",newconfd);
		pid_t pid = fork();
		error_Handling("fork",pid);
		if(pid == 0)
		{
			printf("FATHERPID:%d  CHILDPID:%d\n",getppid(),getpid());
			printf("与IP:| %s |建立连接\n",inet_ntoa(buf_addr.sin_addr));

			while(1)
			{
				/*在这里做一个IO多路复用*/
				fd_set readfds;
				FD_ZERO(&readfds);
				FD_SET(0,&readfds);
				FD_SET(newconfd,&readfds);
				int ret_select = select(newconfd+1,&readfds,NULL,NULL,NULL);
				error_Handling("select",ret_select);
				/*select返回表示有描述符就绪*/
				if(FD_ISSET(0,&readfds))//检查标准输入是否被置位
				{
					ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
					error_Handling("read",ret_read);
					ssize_t ret_write = write(newconfd,&buf_data,sizeof(buf_data));
					error_Handling("write",ret_write);
					printf("我:%s\n",buf_data);
					bzero(&buf_data,sizeof(buf_data));
				}
				if(FD_ISSET(newconfd,&readfds))//检查套接字是否被置位
				{
					ssize_t ret_read = read(newconfd,&buf_data,sizeof(buf_data));
					error_of_read(ret_read,inet_ntoa(buf_addr.sin_addr));
					printf("%s:%s\n",inet_ntoa(buf_addr.sin_addr),buf_data);
					bzero(&buf_data,sizeof(buf_data));
				}
			}
		}
	}
	
	/*关闭套接字*/
	//close(newconfd);
	close(listenfd);

	return 0;
}
void error_Handling(char* func,int retval)
{
	if(retval == -1)
	{
		perror(func);
		exit(1);
	}
}
void error_of_read(int retval,char* IP)
{
	if(retval<0)
	{
		perror("read");
		exit(1);
	}
	if(retval == 0)
	{
		perror("read");
		printf("%s 已断开连接,此进程结束\n",IP);
		exit(1);
	}
}
void sigFun(int sig)
{
	wait(NULL);
	count--;
	printf("有子进程退出已经收尸\n");
}



/***************************/
/*          客户端         */
/***************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void error_Handling(char* func,int retval);

int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		printf("%s IP Port\n",argv[0]);
		exit(1);
	}
	char buf_data[1024] = {};
	/*创建套接字*/
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	error_Handling("socket",sockfd);

	/*连接*/
	struct sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(atoi(argv[2]));
	serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
	int ret_connect = connect(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
	error_Handling("connect",ret_connect);

	/*发送数据*/
	while(1)
	{
		/*在这里做一个IO多路复用*/
		fd_set readfds;
		FD_ZERO(&readfds);
		FD_SET(0,&readfds);
		FD_SET(sockfd,&readfds);
		int ret_select = select(sockfd+1,&readfds,NULL,NULL,NULL);
		error_Handling("select",ret_select);
		/*select返回表示有描述符就绪*/
		if(FD_ISSET(0,&readfds))//检查标准输入是否被置位
		{
			ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
			error_Handling("read",ret_read);
			ssize_t ret_write = write(sockfd,&buf_data,sizeof(buf_data));
			error_Handling("write",ret_write);
			printf("我:%s\n",buf_data);
			bzero(&buf_data,sizeof(buf_data));
		}
		if(FD_ISSET(sockfd,&readfds))//检查套接字是否被置位
		{
			ssize_t ret_read = read(sockfd,&buf_data,sizeof(buf_data));
			error_Handling("read",ret_read);
			printf("%s:%s\n",inet_ntoa(serverAddr.sin_addr),buf_data);
			bzero(&buf_data,sizeof(buf_data));
		}
	}

	/*关闭套接字*/
	close(sockfd);

	return 0;
}

void error_Handling(char* func,int retval)
{
	if(retval == -1)
	{
		perror(func);
		exit(1);
	}
}

六 套接字的缓冲区以及阻塞模式

Socket套接字通信 TCP UDP详解(网络通信)_第4张图片
参考socket套接字及缓冲区详解

1.缓冲区

  • 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区输出缓冲区
  • write()/send()/send to() 函数并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
  • read()/recv()/recefrom() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
  • 每个套接字的I/O缓冲区单独存在。
  • 即使一端关闭套接字,也会继续传送这端套接字输出缓冲区中遗留的数据。
  • 如果一端关闭套接字,这端将丢失输入缓冲区中的数据。
  • 默认情况下,套接字为阻塞模式
//把套接字设置成非阻塞模式
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); 
//非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源

2.使用write/send发送数据

阻塞模式下:

  • 首先会检查输出缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;
  • 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。直到所有数据被写入缓冲区 write()/send() 才能返回。
  • 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
  • send()函数默认情况下会使用Nagle算法。Nagle算法通过将未确认的数据存入缓冲区直到积攒到一定数量一起发送的方法,来降低主机发送零碎小数据包的数目。所以假设send()函数发送数据过快的话,该算法会将一些数据打包后统一发出去。通过setsockopt()的TCP_NODELAY选项来禁用Nagle算法。

非阻塞模式下:

  • write/send不做等待立即返回;
  • write()/send()函数的过程仅仅是将数据拷贝到协议栈的缓冲区而已,如果缓冲区可用空间不够,则尽可能拷贝,返回成功拷贝的大小;
  • 如果缓存区可用空间为0,则返回-1,同时设置errno为EWOULDBLOCK。

3.使用read/recv读取数据

read/recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
阻塞模式下:

  • 首先会检查输入缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;(如果数据正在从输入缓冲区拷贝到用户空间,read/recv也会被阻塞)
  • 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到输入缓冲区满,协议栈不能再接收数据。

非阻塞模式下:

  • write/send不做等待立即返回;
  • 成功返回实际读到的字节数;
  • 如果输入缓冲区中没有数据,返回错误EWOULDBLOCK。

七 总结套接字收发数据的过程

TCP发送数据的过程:首先,TCP是有链接的可靠传输协议,所谓可靠也就是说保证客户端发送的数据服务端都能够收到,并且是按序收到。

  1. 数据首先由应用程序缓冲区复制到发送端的输出缓冲区(位于内核),注意这个过程是用类似write功能的函数完成的。有的人通常看到write成功就以为数据发送到了对端主机,其实这是错误的,write成功仅仅表示数据成功的由应用进程缓冲区复制到了输出缓冲区。

  2. 然后内核协议栈将输出缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制,而是发送端内核协议栈完成,其中包括使用滑动窗口、拥塞控制等功能。

  3. 数据到达接收端主机的输入缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成,其中包括发送ack确认等。

  4. 数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。

思考:如果TCP服务端一直sleep,客户端一直发送数据,会出现什么情况?

  1. 阻塞模式下:
    如果服务端一直sleep不接收数据,而客户端一直write,也就是只能执行上述过程中的前三步,这样最终结果肯定是接收端的输入缓冲区和发送端的输出缓冲区都被填满,这样write就无法继续将数据从应用程序复制到发送端的输出缓冲区了,从而使进程进入睡眠。
  2. 非阻塞情况下:
    服务端一直sleep,随着客户端write,接收端的输入缓冲区和发送端的输出缓冲区会被填满。当发送端的输出缓冲区的可用空间为0时,write立即返回-1,并将errno置为EWOULDBLOCK。

“华清远见” http://www.hqyj.com/学习更多编程知识

你可能感兴趣的