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;
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章