Linux应用编程和网络编程(11)-------TCP/IP协议的简单学习与应用socket接口编程

1.网络的分层结构

因为网络是一种非常复杂的通信方式,所以要通过分层来进行开发难度的降低。因此我们在研究网络通信时,一定要在同一个层次进行研究,不能跨层次研究,比如分析客户端和服务器的收发时,要分析API层次时,两部分都要统一在这个层次进行分析,而不能是一端分析API接口,另一端却去分析驱动了。一般情况下,我们在网络编程时最关注的是应用层,传输层只需要了解即可。

2.BS和CS
(1)CS架构介绍(client server,客户端服务器架构)。比如QQ,360网盘之类的(在电脑上或手机上用软件登录的)。
(2)BS架构介绍(broswer server,浏览器服务器架构)。比如在线版的QQ,网页版360网盘(用浏览器打开的)。

一,TCP协议的简单学习

TCP协议
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议

1、关于TCP理解的重点

  1. TCP协议工作在OSI七层模型的传输层对上服务socket接口对下调用IP层.
  2. TCP协议面向连接,通信前必须先3次握手建立连接关系后才能开始通信。
  3. TCP协议提供可靠传输,不怕丢包、乱序等。

2、TCP如何保证可靠传输

  1. TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
  2. TCP的接收方收到数据包后会ack给发送方,若发送方未收到ack会丢包重传
  3. TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏
  4. TCP会根据网络带宽来自动调节适配速率(滑动窗口技术)
  5. 发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。

建立TCP需要三次握手才能建立,而断开连接则需要四次握手。整个过程如下图所示:

在这里插入图片描述

3、TCP的三次握手

  1. 首先Client端发送连接请求报文Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。
  2. 建立连接的条件:服务器listen时客户端主动发起connect

4、TCP的四次挥手

  1. 断开连接的条件:服务器或者客户端都可以主动发起关闭
  2. 断开过程:假设客户端先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。客户端发送释放报文FIN [第一次] 。服务器收到释放报文后发出确认报文ACK [第二次] 。服务器发出的连接释放报文FIN,并且还附带上次已发送过的确认号ACK [第三次] 。客户端在收到服务器的连接释放报文段后,发送确认报文ACK [第四次] 。

注:这些握手协议已经封装在TCP协议内部,socket编程接口平时不用管

5、基于TCP通信的服务模式

  1. 具有公网IP地址的服务器(或者使用动态IP地址映射技术)
  2. 服务器端socket、bind、listen后处于监听状态
  3. 客户端socket后,直接connect去发起连接
  4. 服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起
  5. 双方均可发起关闭连接
  6. 常见的使用了TCP协议的网络应用:http(相当于一个应用程序,用来传输文本信息)、ftp、QQ服务器和mail服务器。这些需要很高可靠的应用,底层都是基于TCP协议的。

二,socket编程接口介绍

1、建立连接

  1. socket:socket函数类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。
  2. == bind==:用来进行绑定的函数,把本地的IP地址和socket进行绑定。功能类似于fctrl函数,是用来改变属性的函数
  3. listen:监听一个端口,监听的是在bind时绑定的那个地址
  4. accept返回值是一个fd,accept正确返回就表示我们已经和前来连接我的客户端之间建立了一个TCP连接了,以后我们就要通过这个连接来和客户端进行读写操作,读写操作就需要一个fd,这个fd就由accept来返回了。(阻塞的位置)
  5. connect:用来连接服务器的(客户端那边用)。

2、发送和接收

  1. send/write发送数据。(send比write就多了一个flag,只有支持一些特殊协议时会用到flag,普通情况写0即可)
  2. recv/read:接收数据。(在网络中发送有点像是写文件,在网络中接收有点像是收文件)

3、辅助性函数

  1. ==inet_aton、inet_addr、inet_ntoa ==:(点分十进制和32位二进制形式互相转化,不支持IPv6)
  2. inet_ntop、inet_pton:(点分十进制和32位二进制形式互相转化,支持IPv6)

4、表示IP地址相关数据结构

  1. 都定义在 netinet/in.h

  2. struct sockaddr,这个结构体是网络编程接口中用来表示一个IP地址的,注意这个IP地址是兼容IPv4和IPv6

  3. typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型

  4. struct in_addrstruct in_addr { in_addr_t s_addr; };

  5. struct sockaddr_in

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
  };
  
  1. struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充

三,IP地址格式转换函数实践

在进行格式转换时,这些函数,默认都使用大端模式

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define IPADDR	"192.168.1.102"

// 0x66		01	a8		c0
// 102		1	168		192
// 网络字节序,其实就是大端模式


int main(void)
{
	//使用inet_ntop来转换
	struct in_addr addr = {0};
	char buf[50] = {0};
	
	addr.s_addr = 0x6601a8c0;
	
	inet_ntop(AF_INET, &addr, buf, sizeof(buf));

	printf("ip addr = %s.\n", buf);   //192.168.1.102
	
	
#if 0
	// 使用inet_pton来转换
	int ret = 0;
	struct in_addr addr = {0};
	
	ret = inet_pton(AF_INET, IPADDR, &addr);
	if (ret != 1)
	{
		printf("inet_pton error\n");
		return -1;
	}
	
	printf("addr = 0x%x.\n", addr.s_addr);  //0x6601a8c0
	
#endif
	
#if 0    //使用inet_addr来转换
	in_addr_t addr = 0;
	
	addr = inet_addr(IPADDR);
	
	printf("addr = 0x%x.\n", addr);		// 0x6601a8c0
	
#endif	

	return 0;
}




四,网络编程实战


概念端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的

探讨:如何让服务器和客户端好好沟通
(1)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收
(2)必须了解到的一点:client和server之间的通信是异步的,这就是问题的根源
(3)解决方案:依靠应用层协议来解决。说白了就是我们server和client事先做好一系列的通信约定。

自定义应用层协议
1、自定义应用层协议第一步:规定发送和接收方法
(1)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合
(2)整个连接的通信就是由N多个回合组成的。

2、自定义应用层协议第二步:定义数据包格式
3、常用应用层协议:http、ftp······

下面以一个例子展示网络编程:客户端向服务器注册学生的基本信息(发送一个数据包),服务器回应一个数据包表示接收完成(展示学生的基本信息)。

服务器端代码:server.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERPORT		9003
#define SERADDR		"192.168.1.104"		// ifconfig看到的
#define BACKLOG		100		//貌似是最大能容纳的连接数

char recvbuf[100];

#define CMD_REGISTER	1001	// 注册学生信息
#define CMD_CHECK		1002	// 检验学生信息
#define CMD_GETINFO		1003	// 获取学生信息

#define STAT_OK			30		// 回复ok
#define STAT_ERR		31		// 回复出错了

typedef struct commu
{
	char name[20];		// 学生姓名
	int age;			// 学生年龄
	int cmd;			// 命令码
	int stat;			// 状态信息,用来回复
}info;


int main(void)
{
	int sockfd = -1, ret = -1, clifd = -1;
	socklen_t len = 0;
	//这里的结构为sockaddr_in结构体包含sin_port和sin_addr结构体,sin_addr结构体包含s_addr
	struct sockaddr_in seraddr = {0};
	struct sockaddr_in cliaddr = {0};
	
	// 第1步:先socket打开文件描述符	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("socket");
		return -1;
	}
	printf("socketfd = %d.\n", sockfd);
	
	// 第2步:bind绑定sockefd和当前电脑的ip地址&端口号
	seraddr.sin_family = AF_INET;		// 设置地址族为IPv4
	seraddr.sin_port = htons(SERPORT);	// 设置地址的端口号信息
	seraddr.sin_addr.s_addr = inet_addr(SERADDR);	// 设置IP地址
	ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
	if (ret < 0)
	{
		perror("bind");
		return -1;
	}
	printf("bind success.\n");
	
	// 第3步:listen监听端口
	ret = listen(sockfd, BACKLOG);		// 阻塞等待客户端来连接服务器
	if (ret < 0)
	{
		perror("listen");
		return -1;
	}
	
	// 第4步:accept阻塞等待客户端接入
	clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
	printf("连接已经建立,client fd = %d.\n", clifd);

	// 客户端反复给服务器发
	while (1)
	{
		info st;
		// 回合中第1步:服务器收
		ret = recv(clifd, &st, sizeof(info), 0);

		// 回合中第2步:服务器解析客户端数据包,然后干活,
		if (st.cmd == CMD_REGISTER)
		{
			printf("用户要注册学生信息\n");
			printf("学生姓名:%s,学生年龄:%d\n", st.name, st.age);
			// 在这里服务器要进行真正的注册动作,一般是插入数据库一条信息
			
			// 回合中第3步:回复客户端
			st.stat = STAT_OK;
			ret = send(clifd, &st, sizeof(info), 0);
		}
		
	}

	return 0;
}

客户端代码:client.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERADDR		"192.168.1.104"		// 服务器开放给我们的IP地址和端口号
#define SERPORT		9003

//发送、接收缓冲区
char sendbuf[100];
char recvbuf[100];

#define CMD_REGISTER	1001	// 注册学生信息
#define CMD_CHECK		1002	// 检验学生信息
#define CMD_GETINFO		1003	// 获取学生信息

#define STAT_OK			30		// 回复ok
#define STAT_ERR		31		// 回复出错了

typedef struct commu
{
	char name[20];		// 学生姓名
	int age;			// 学生年龄
	int cmd;			// 命令码
	int stat;			// 状态信息,用来回复
}info;

int main(void)
{
	int sockfd = -1, ret = -1;
	//这个结构体是网络编程接口中用来表示一个IP地址的,
	//这个IP地址是兼容IPv4和IPv6的
	struct sockaddr_in seraddr = {0};
	struct sockaddr_in cliaddr = {0};
	
	// 第1步:创建socket(AF_INET:使用IPv4进行通信,SOCK_STREAM:TCP)
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("socket");
		return -1;
	}
	printf("socketfd = %d.\n", sockfd);
	
	// 第2步:connect链接服务器,向结构体填充服务器信息
	seraddr.sin_family = AF_INET;		// 设置地址族为IPv4
	seraddr.sin_port = htons(SERPORT);	// 设置地址的端口号信息(检测大小端并调整)
	seraddr.sin_addr.s_addr = inet_addr(SERADDR);	// 设置IP地址
	ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
	if (ret < 0)
	{
		perror("connect");
		return -1;
	}
	printf("成功建立连接\n");

	while (1)
	{
		// 回合中第1步:客户端给服务器发送信息
		info st1;
		printf("请输入学生姓名\n");
		scanf("%s", st1.name);
		printf("请输入学生年龄");
		scanf("%d", &st1.age);
		st1.cmd = CMD_REGISTER;
		ret = send(sockfd, &st1, sizeof(info), 0);
		printf("已发送%s学生的信息\n",st1.name);
		
		// 回合中第2步:客户端接收服务器的回复
		memset(&st1, 0, sizeof(st1));
		ret = recv(sockfd, &st1, sizeof(st1), 0);
		
		// 回合中第3步:客户端解析服务器的回复,再做下一步定夺
		if (st1.stat == STAT_OK)
		{
			printf("注册学生信息成功\n");
		}
		else
		{
			printf("注册学生信息失败\n");
		}

	}

	return 0;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值