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