基于? C/S :客戶端(client)/服務器端(server)
1.流程
? ? ? ?
2.? 函數接口
所有函數所需頭文件:
#include <sys/types.h> #include <sys/socket.h>
系統定義好了用來存儲網絡信息的結構體
ipv4通信使用的結構體:struct sockaddr_in
我們只需要直接定義結構體變量即可
2.1 創建套接字socket()
int socket(int domain, int type, int protocol);
功能:創建套接字
參數:domain:協議族AF_UNIX, AF_LOCAL 本地通信AF_INET ipv4AF_INET6 ipv6type:套接字類型SOCK_STREAM:流式套接字SOCK_DGRAM:數據報套接字SOCK_RAW:原始套接字protocol:協議 一般填0 自動匹配底層 根據type系統默認自動幫助匹配對應協議傳輸層:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP網絡層:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)返回值:成功 文件描述符-- > sockfd (用于連接)失敗 -1,更新errno
?注意:TCP服務器端有兩類文件描述符 !!!
? ? ? ? 一類用于連接的文件描述符(sockfd-->socket函數返回值) 只有一個
? ? ? ? 一類用于通信的文件描述符(acceptfd-->accept函數返回值) 可以多個
2.2綁定套接字bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:綁定
參數:socket:套接字addr:用于通信結構體 (提供的是通用結構體,需要根據選擇通信方式,填充對應結構體-通信當時socket第一個參數確定) addrlen:結構體大小
返回值:成功 0 失敗-1,更新errno
由于系統定義好的記錄網絡信息的結構體是struct sockaddr_in類型,因此,bind第二個參數使用時結構體變量地址的時候要強制類型轉換
2.3監聽listen()
int listen(int sockfd, int backlog);
功能:監聽,將主動套接字變為被動套接字
參數:sockfd:套接字backlog:(目前已無具體作用,寫個正數即可)同時響應客戶端請求鏈接的最大個數,不能寫0.不同平臺可同時鏈接的數不同,一般寫6-8個(隊列1:保存正在連接)(隊列2,連接上的客戶端)返回值:成功 0 失敗-1,更新errno
注意:listen作用:主動套接字變為被動套接字!!!
2.4接收客戶端連接請求 accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函數,阻塞等待客戶端的連接請求,如果有客戶端連接,
則accept()函數返回,返回一個用于通信的套接字文件描述符;
參數:Sockfd :套接字addr: 鏈接客戶端的ip和端口號如果不需要關心具體是哪一個客戶端,那么可以填NULL;addrlen:結構體的大小如果不需要關心具體是哪一個客戶端,那么可以填NULL;
返回值: 成功:文件描述符; //用于通信失敗:-1,更新errno
2.5接受消息recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收數據
參數: sockfd: acceptfd ;buf 存放位置len 大小flags 一般填0,相當于read()函數MSG_DONTWAIT 非阻塞
返回值: < 0 失敗出錯 更新errno==0 表示客戶端退出>0 成功接收的字節個數
2.6發送消息send()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:發送數據
參數:sockfd:socket函數的返回值buf:發送內容存放的地址len:發送內存的長度flags:如果填0,相當于write();
2.7連接服務器connect()
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于連接服務器;
參數:sockfd:socket函數的返回值addr:填充的結構體是服務器端的;addrlen:結構體的大小
返回值 -1 失敗,更新errno正確 0
2.8 關閉套接字 close()
即關閉套接字文件
close(文件描述符);
3.服務器端
?按照流程:
(1)創建流式套接字socket()
? ? ?
(2)指定網絡信息
?(3)綁定套接字bind()
(4) 監聽listen()
(5) 等待客戶連接信息accept()
?注意:
在服務器端使用客戶的網絡信息時:
(6)收發消息 send() recv()
(7) 關閉套接字
源代碼:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>// $$ 服務器端 $$ int main(int argc, char const *argv[])
{/*創建流式套接字*/int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("創建套接字成功\n");}/*指定服務器網絡信息 使用的協議族(IPv4-->AF_INET)、IP地址、端口號等*/// 服務器的網絡信息通過一個系統定義好的結構體來描述struct sockaddr_in saddr; // 定義一個結構體變量saddr.sin_family = AF_INET; // 確定協議族-->IPv4saddr.sin_port = htons(atoi(argv[1])); // 確定使用的端口號saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 確定服務器IP地址/*綁定套接字*/int t1 = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (t1 < 0){perror("bind err");return -1;}else{printf("綁定套接字成功\n");}/*監聽*/int t2 = listen(sockfd, 6); // 將默認的主動套接字變為被動套接字if (t2 < 0){printf("listen err");return -1;}else{printf("監聽中\n");}int acceptfd;char buf[128] = "";int ret;// 定義一個結構體變量來存接收到的客戶信息struct sockaddr_in caddr;int len = sizeof(caddr); // len是記錄客戶信息的結構體的大小while (1){/*阻塞等待接收客戶端的連接請求,并將連接成功的客戶端信息寫入到結構體變量caddr中*/acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){printf("accept err");return -1;}else{printf("等待接收客戶端請求\n");}printf("客戶IP:%s 端口號:%d \n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));while (1){/*接收消息*/ret = recv(acceptfd, buf, 128, 0); // 0-->相當于read(acceptfd,buf,128)if (ret < 0){perror("recv err");return -1;}else if (ret == 0){printf("客戶退出\n");break;}else{printf("%s 接收成功\n", buf);memset(buf, 0, sizeof(buf));}}close(acceptfd);}/* 關閉套接字 */close(sockfd);return 0;
}
4.客戶端
按照流程:
(1)創建流式套接字socket()
(2)指定服務器網絡信息
(3)連接服務器connect()
(4)發送接受消息 send()? recv()
(5)關閉套接字
源代碼:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
// $$ 客戶端 $$ int main(int argc, char const *argv[])
{/*創建流式套接字*/int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}else{printf("創建套接字成功\n");}/*指定服務器的網絡信息*/struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr(argv[2]);/* 請求連接服務器*/int t = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));if (t < 0){perror("connect err");return -1;}else{printf("connect success\n");}char buf[128] = "";/* 發送消息 */while (1){fgets(buf, sizeof(buf), stdin);if (buf[strlen(buf) - 1] == '\n'){buf[strlen(buf) - 1] = '\0';}if (strcmp(buf, "quit") == 0){break;}send(sockfd, buf, sizeof(buf), 0); // 0-->相當于write(sockfd,buf,sizeof(buf))}/* 關閉套接字 */close(sockfd);return 0;
}
5.TCP粘包問題
tcp粘包
tcp拆包
6.三次握手四次揮手
三次握手建立連接
第一次握手:客戶通過調用connect進行主動打開(active open)。這引起客戶TCP發送一個SYN(表示同步)分節(SYN=J),它告訴服務器客戶將在連接中發送數據的初始序列號。并進入SYN_SEND狀態,等待服務器的確認。
第二次握手:服務器必須確認客戶的SYN,同時自己也得發送一個SYN分節,它含有服務器將在同一連接中發送的數據的初始序列號。服務器以單個字節向客戶發送SYN和對客戶SYN的ACK(表示確認),此時服務器進入SYN_RECV狀態。
第三次握手:客戶收到服務器的SYN+ACK。向服務器發送確認分節,此分節發送完畢,客戶服務器進入ESTABLISHED狀態,完成三次握手。
?