socket编程(TCP)

TCP协议的特点

1、面向连接的流式协议;可靠、出错重传、且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接

TCP与UDP的差异:
在这里插入图片描述
TCP C/S架构
在这里插入图片描述

1、TCP客户端

1.1、创建tcp套接字

在这里插入图片描述

1.2、做为客户端需要具备的条件

1、知道“服务器”的ip、port
2、Connect主动连接“服务器”
3、需要用到的函数
socket—创建“主动TCP套接字”
connect—连接“服务器”
send—发送数据到“服务器”
recv—接受“服务器”的响应
close—关闭连接

1.3、connect链接服务器的函数

int connect(int sockfd,const struct sockaddr *addr,socklen_t len);
功能:
    主动跟服务器建立链接
参数:
    sockfd:socket套接字
    addr:   连接的服务器地址结构
    len:  地址结构体长度
返回值:
    成功:0    失败:其他
头文件
	#include <sys/socket.h>

注意:

1、connect建立连接之后不会产生新的套接字
2、连接成功后才可以开始传输TCP数据
3、头文件:#include <sys/socket.h>

1.4、send函数

ssize_t send(int sockfd, const void* buf,size_t nbytes, int flags);
功能:
    用于发送数据
参数:
    sockfd: 已建立连接的套接字
    buf:    发送数据的地址
    nbytes:  发送缓数据的大小(以字节为单位)
    flags:    套接字标志(常为0)
返回值:
    成功发送的字节数
头文件:
    #include <sys/socket.h>

1.5、recv函数

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
功能:
    用于接收网络数据
参数:
    sockfd:套接字
    buf: 接收网络数据的缓冲区的地址
    nbytes: 接收缓冲区的大小(以字节为单位)
    flags: 套接字标志(常为0)
返回值:
    成功接收到字节数
头文件:
    #include <sys/socket.h>

案例:TCP客户端

#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <fcntl.h>
//TCP客户端
int main()
{
	//创建一个TCP套接字 SOCK_STREAM
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("sockfd = %d\n", sockfd);
	
	//bind是可选的
	struct sockaddr_in my_addr;
	bzero(&my_addr,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(9000);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(sockfd,(struct sockaddr *)&my_addr,sizeof(my_addr));
	
	//connect链接服务器
	struct sockaddr_in ser_addr;
	bzero(&ser_addr,sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(8000);//服务器的端口
	ser_addr.sin_addr.s_addr = inet_addr("192.168.0.110");//服务器的IP
	//如果sockfd没有绑定固定的IP以及端口 在调用connect时候 系统给sockfd分配自身IP以及随机端口
	connect(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr));
	
	//给服务器发送数据 send
	printf("发送的消息:");
	char buf[128]="";
	fgets(buf,sizeof(buf),stdin);
	buf[strlen(buf)-1]=0;
	send(sockfd, buf, strlen(buf), 0);
	
	//接收服务器数据 recv
	char msg[128]="";
	recv(sockfd, msg,sizeof(msg), 0);
	printf("服务器的应答:%s\n", msg);
	
	//关闭套接字
	close(sockfd);
	return 0;
}

运行结果:
在这里插入图片描述

2、TCP服务器

做为TCP服务器需要具备的条件

1、具备一个可以确知的地址 bind
2、让操作系统知道是一个服务器,而不是客户端 listen
3、等待连接的到来 accpet
对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始.

2.1、listen 函数

int listen(int sockfd, int backlog);
功能:
    将套接字由主动修改为被动。
    使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接。
参数:
    sockfd: socket监听套接字
    backlog:连接队列的长度
返回值:
    成功:返回0

2.2、accept函数

int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
功能:
    从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
参数:
    sockfd: socket监听套接字
    cliaddr: 用于存放客户端套接字地址结构
    addrlen:套接字地址结构体长度的地址
返回值:
    已连接套接字
头文件:
    #include <sys/socket.h>

注意:

返回的是一个已连接套接字,这个套接字代表当前这个连接

在这里插入图片描述

案例:

#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <fcntl.h>

int main()
{
	//1、创建一个tcp监听套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	//2、使用bind函数 给监听套接字 绑定固定的ip以及端口
	struct sockaddr_in my_addr;
	bzero(&my_addr,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
	
	//3、使用listen创建连接队列 主动变被动
	listen(sockfd, 10);
	
	//4、使用accpet函数从连接队列中 提取已完成的连接 得到已连接套接字
	struct sockaddr_in cli_addr;
	socklen_t cli_len = sizeof(cli_addr);
	int new_fd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);
	//new_fd代表的是客户端的连接   cli_addr存储是客户端的信息
	char ip[16]="";
	inet_ntop(AF_INET,&cli_addr.sin_addr.s_addr, ip,16);
	printf("客户端:%s:%hu连接了服务器\n",ip,ntohs(cli_addr.sin_port));
	
	//5、获取客户端的请求 以及 回应客户端
	char buf[128]="";
	recv(new_fd, buf,sizeof(buf),0);
	printf("客户端的请求为:%s\n",buf);
	
	send(new_fd,"recv", strlen("recv"), 0);
	
	
	//6、关闭已连接套接字
	close(new_fd);
	
	//7、关闭监听套接字
	close(sockfd);
	
	return 0;
}

运行结果:
在这里插入图片描述

3、TCP的三次握手 四次挥手

3.1、TCP的三次握手 客户端 connec函数调用的时候发起

SYN是一个链接请求:是TCP报文中的某一个二进制位
在这里插入图片描述
在这里插入图片描述
第一次握手:客户端 发送SYN请求 链接服务器
第二次握手:服务器 ACK回应客户端的链接请求 同时 服务器给客户端发出链接请求
第三次握手:客户端 ACK回应 服务器的链接请求

3.2、四次挥手 调用close 激发 底层发送FIN关闭请求

不缺分客户端 或 服务器先后问题。
在这里插入图片描述
第一次挥手:A调用close 激发底层 发送FIN关闭请求 并且A处于半关闭状态
第二次挥手:B的底层给A回应ACK 同时导致B的应用层recv/read收到0长度数据包
第三次挥手:B调用close 激发底层给A发送FIN关闭请求 并且B处于半关闭状态
第四次挥手:A的底层给B回应ACK 同时 A处于完全关闭状态,B收到A的ACK也处于完全关闭状态

3.3、close 关闭套接字

在这里插入图片描述

4、TCP并发服务器

并发服务器:同时 服务于 多个客户端
TCP并发服务器:本质是TCP服务器,同时服务于多个客户端
TCP并发服务器的注意点:
TCP服务器、提取多个客户端、开启进程或线程处理每个客户端

4.1、多线程(常用)

在这里插入图片描述

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
//TCP并发ECHO服务器(并发回执服务器---客户端给服务器发啥 服务器就给客户端回啥)
void* deal_client_fun(void *arg)//arg = &new_fd
{
	//并发服务器的核心服务代码(各不相同)
	//通过arg获得已连接套接字
	int fd = *(int *)arg;
	while(1)//以下语句是服务器的核心代码
	{
		//获取客户端请求
		char buf[128]="";
		int len = recv(fd,buf,sizeof(buf), 0);
		if(len == 0)
			break;
		//回应客户端
		send(fd, buf, len, 0);
	}
	
	close(fd);
}
int main()
{
	//1、创建tcp监听套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("socket");
	}
	int yes = 1;
     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,sizeof(yes));
     
	//2、给TCP监听套接字 bind固定的IP以及端口信息
	struct sockaddr_in my_addr;
	bzero(&my_addr,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	int ret = bind(sockfd, (struct sockaddr *)&my_addr,sizeof(my_addr));
	if(ret == -1)
	{
		perror("bind");
	}
	//3、调用listen 将sockfd主动变被动  同时创建链接队列
	listen(sockfd, 10);
	
	//4、提取完成链接的客户端 accept
	//accept调用一次 只能提取一个客户端
	while(1)
	{
		struct sockaddr_in cli_addr;
		socklen_t cli_len = sizeof(cli_addr);
		int new_fd = accept(sockfd,(struct sockaddr *)&cli_addr , &cli_len);
		
		//遍历客户端的信息ip port
		unsigned short port=ntohs(cli_addr.sin_port);
		char ip[16]="";
		inet_ntop(AF_INET,&cli_addr.sin_addr.s_addr, ip, 16);
		printf("已有客户端:%s:%hu连接上了服务器\n", ip, port);
		
		//对每一个客户端 开启一个线程 单独的服务器客户端
		pthread_t tid;
		pthread_create(&tid,NULL, deal_client_fun, (void *)&new_fd);
		//线程分离
		pthread_detach(tid);
	}	
	
	//关闭监听套接字
	close(sockfd);
	
	return 0;
}

运行结果:
在这里插入图片描述
上述代码 如果客户端 正常退出 不会有啥影响,但是如果服务器 意外退出 绑定的端口信息来不及释放,就会造成 系统临时占用服务器上次bind的端口,如果在5~6分钟之内再次运行服务器 这是导致新运行的服务器 bind失败

在这里插入图片描述

4.2、解决上述问题:端口复用

服务器的进程网络资源 任然被占用 一般1分钟作用释放

int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,sizeof(yes));

将上面的两句话添加到socket只有 bind函数之前

4.3、并发服务器 多进程实现

在这里插入图片描述

在这里插入图片描述

案例

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
//TCP并发ECHO服务器(并发回执服务器---客户端给服务器发啥 服务器就给客户端回啥)
void deal_client_fun(int fd)//fd = new_fd
{
	while(1)//以下语句是服务器的核心代码
	{
		//获取客户端请求
		char buf[128]="";
		int len = recv(fd,buf,sizeof(buf), 0);
		if(len == 0)
			break;
		//回应客户端
		send(fd, buf, len, 0);
	}
	
	return;
}
int main()
{
	//1、创建tcp监听套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("socket");
	}
	//端口复用
	int yes = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,sizeof(yes));
	
	//2、给TCP监听套接字 bind固定的IP以及端口信息
	struct sockaddr_in my_addr;
	bzero(&my_addr,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	int ret = bind(sockfd, (struct sockaddr *)&my_addr,sizeof(my_addr));
	if(ret == -1)
	{
		perror("bind");
	}
	//3、调用listen 将sockfd主动变被动  同时创建链接队列
	listen(sockfd, 10);
	
	//4、提取完成链接的客户端 accept
	//accept调用一次 只能提取一个客户端
	while(1)
	{
		struct sockaddr_in cli_addr;
		socklen_t cli_len = sizeof(cli_addr);
		int new_fd = accept(sockfd,(struct sockaddr *)&cli_addr , &cli_len);
		
		//遍历客户端的信息ip port
		unsigned short port=ntohs(cli_addr.sin_port);
		char ip[16]="";
		inet_ntop(AF_INET,&cli_addr.sin_addr.s_addr, ip, 16);
		printf("已有客户端:%s:%hu连接上了服务器\n", ip, port);
		
		pid_t pid;
		if(fork() == 0)//子进程  服务器客户端 不需要监听套接字
		{
			//关闭监听套接字
			close(sockfd);
			
			//服务于客户端
			deal_client_fun(new_fd);
			
			//关闭已连接套接字
			close(new_fd);
			_exit(-1);
		}
		else//父进程
		{
			//监听新的连接到来 不需要和客户端通信 必须关闭已连接套接字new_fd
			close(new_fd);
		}
	}	
	
	//关闭监听套接字
	close(sockfd);
	
	return 0;
}

运行结果:

在这里插入图片描述

总结:

TCP并发服务器 进程版:父子进程 资源独立 某个进程结束 不会影响已有的进程 服务器更加稳定 代价多进程 会消耗很多资源。
TCP并发服务器 线程版:线程共享进程资源 资源开销小 但是一旦主进程结束 所有线程都会结束 服务器先对进程 不是那么稳定

临时复习:已连接套接字 和 accpet中返回的客户端地址结构分析

在这里插入图片描述

5、HTTP协议

HTTP基于TCP
在这里插入图片描述

5.1、HTTP协议的概述

5.1.1、web服务器简介

Web服务器又称WWW服务器、网站服务器等
特点
使用HTTP协议与客户机浏览器进行信息交流
不仅能存储信息,还能在用户通过web浏览器提供的信息的基础上运行脚本和程序
该服务器需可安装在UNIX、Linux或Windows等操作系统上
著名的服务器有Apache、Tomcat、 IIS等

5.1.2、HTTP协议

Webserver—HTTP协议
概念
一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
特点
1、支持C/S架构
2、简单快速:客户向服务器请求服务时,只需传送请求方法和路径 ,常用方法:GET、POST
3、无连接:限制每次连接只处理一个请求
4、无状态:即如果后续处理需要前面的信息,它必须重传,这样可能导致每次连接传送的数据量会增大

5.2、Webserver 通信过程

我们写的是服务器端 必须是TCP并发服务器 客户端 由浏览器充当
在这里插入图片描述

5.3、Web编程开发

网页浏览(使用GET方式)
客户端浏览器请求:
在这里插入图片描述
在这里插入图片描述
服务器收到的数据(浏览器发出的文件请求):
在这里插入图片描述
服务器应答的格式:请求成功 服务器打开文件成功 给浏览器发送的报文
在这里插入图片描述
服务器应答的格式:请求失败 打开本地文件失败 给浏览器发报文
在这里插入图片描述

案例:webserver服务器

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
char head[] = "HTTP/1.1 200 OK\r\n"						\
				  "Content-Type: text/html\r\n"				\
				  "\r\n";
char err[]=	"HTTP/1.1 404 Not Found\r\n"			\
					"Content-Type: text/html\r\n"			\
					"\r\n"								\
					"<HTML><BODY>File not found</BODY></HTML>";
void *deal_client_fun(void *arg)//arg=new_fd
{
	int new_fd = (int)arg;
	
	//1、recv获取客户端的请求(只需要调用一次)
	unsigned char buf[512]="";
	recv(new_fd,buf,sizeof(buf), 0);
	
	//2、解析buf 提取所请求的文件名
	char file_name[128]="./html/";
	sscanf(buf,"GET /%s", file_name+7);
	if(file_name[7]=='\0')
		char file_name[128]="./html/index.html";
	
	//3、打开本地文件
	int fd = open(file_name, O_RDONLY);
	if(fd < 0)//打开文件失败
	{
		perror("open");
		//发送失败报文给客户端
		send(new_fd, err, strlen(err), 0);
	}
	else//打开本地文件成功
	{
		//发送成功报文 请准备接受
		send(new_fd, head, strlen(head), 0);
		
		//不停的给浏览器客户端 发送文件数据
		while(1)
		{
			//从本地文件读取数据
			unsigned char buf[1024]="";
			int ret = read(fd,buf,sizeof(buf));
			printf("ret=%d\n", ret);
			if(ret<1024)//文件末尾 将数据发送出去
			{
				send(new_fd,buf,ret,0);
				break;
			}
			send(new_fd,buf,ret,0);
		}
		
		//关闭本地文件描述符
		close(fd);
	}
	
	close(new_fd);
	return NULL;
}

//运行的方式:./a.out 8000
int main(int argc,char *argv[])
{
	if(argc != 2)
	{
		printf("./a.out 8000\n");
		return 0;
	}
	
	//1、创建TCP监听套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("socket");
		return 0;
	}
	
	//2、端口复用
	int yes = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,sizeof(yes));
	
	//3、给服务器绑定固定的IP以及端口
	struct sockaddr_in my_addr;
	bzero(&my_addr,sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(atoi(argv[1]));
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	int ret = bind(sockfd, (struct sockaddr *)&my_addr,sizeof(my_addr));
	if(ret == -1)
	{
		perror("bind");
	}
	
	//4、使用listen函数 将套接字 由主动变被动 创建连接队列
	listen(sockfd, 15);
	
	
	//5、while不停的提取客户端 产生已连接套接字
	while(1)
	{
		struct sockaddr_in cli_addr;
		socklen_t cli_len = sizeof(cli_addr);
		int new_fd = accept(sockfd,(struct sockaddr *)&cli_addr , &cli_len);
		
		//遍历客户端的信息ip port
		unsigned short port=ntohs(cli_addr.sin_port);
		char ip[16]="";
		inet_ntop(AF_INET,&cli_addr.sin_addr.s_addr, ip, 16);
		printf("已有客户端:%s:%hu连接上了服务器\n", ip, port);
		
		//创建线程 服务于客户端
		pthread_t tid;
		pthread_create(&tid,NULL, deal_client_fun, (void *)new_fd);
		pthread_detach(tid);
	}
	
	//关闭监听套接字
	close(sockfd);
	return 0;
}

在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值