TCP通信:
1. TCP 協(xié)議通信交互流程:
具體的流程如下:
(1)服務(wù)器根據(jù)地址類型(ipv4、ipv6)、socket 類型、協(xié)議創(chuàng)建 socket.
(2)服務(wù)器為 socket 綁定 ip 地址和端口號。
(3)服務(wù)器 socket 監(jiān)聽端口號的請求,隨時(shí)準(zhǔn)備接受來自客戶端的連接,此時(shí)服務(wù)器的 socket
處于關(guān)閉狀態(tài)。
(4)客戶端創(chuàng)建 socket。
(5)客戶端打開 socket,根據(jù)服務(wù)器 ip 地址和端口號試圖與服務(wù)器的 socket 建立連接。
(6)服務(wù)器的 socket 接收到來自客戶端的 socket 連接請求之后,被動打開開始接收客戶端的請
求,直到客戶端返回連接建立成功的信息。在這個(gè)過程中,服務(wù)器的 socket 進(jìn)入阻塞狀態(tài),也就
是 accept()方法一直到客戶端返回連接信息后才返回開始接收下一個(gè)客戶端的連接請求。
(7)客戶端連接成功,向服務(wù)器發(fā)送連接狀態(tài)信息。
(8)服務(wù)器用 accept() 方法返回,連接成功,接收來自客戶端的連接請求。
(9)客戶端用 send()方法向 socket 中寫入信息。
(10)服務(wù)器通過 recv()方法從 socket 中讀取信息。
(11)客戶端通過 close()方法關(guān)閉 socket。
(12)服務(wù)器通過 close()方法關(guān)閉 socket。
第一次握手:建立連接時(shí),客戶端發(fā)送 SYN 包(SYN=J)到服務(wù)器,并進(jìn)入 SYNSEND 狀態(tài),
等待服務(wù)器確認(rèn)。第二次握手:服務(wù)器收到 SYN 包,必須確認(rèn)客戶的 SYN ( ACK=J+1),同時(shí)自己
也發(fā)送一個(gè) SYN 包( SYN=K),即 SYN+ACK 包,此時(shí)服務(wù)器進(jìn)入 SYN RECV 狀態(tài)。第三次握手:
客戶端收到服務(wù)器的 SYN+ACK 包,向服務(wù)器發(fā)送確認(rèn)包 ACK(ACK=K+I ),此包發(fā)送完畢,客戶
端和服務(wù)器進(jìn)入 ESTABLISHED 狀態(tài),完成 3 次握手。
網(wǎng)絡(luò)層的 IP 地址可以唯一標(biāo)識網(wǎng)絡(luò)中的主機(jī),而傳輸層的“協(xié)議+端口”可以唯一標(biāo)識主機(jī) 中的應(yīng)用程序(進(jìn)程)。這樣利用三元組(ip 地址,協(xié)議,端口)就可以標(biāo)識網(wǎng)絡(luò)的進(jìn)程了,網(wǎng)絡(luò) 中的進(jìn)程通信就可以利用這個(gè)標(biāo)志與其他進(jìn)程進(jìn)行交互 。網(wǎng)絡(luò)中的進(jìn)程是通過 socket 來通信的, socket 是“ open-write/read-close ”模式的一種實(shí)現(xiàn),socket 即是一種特殊的文件, 一些 socket 函數(shù)就是對其進(jìn)行的操作(讀/寫、打開、關(guān)閉)。TCP 服務(wù)器端依次調(diào)用 socket()、bind()、 listen()之后,就會監(jiān)聽指定的 socket 地址了。TCP 客戶端依次調(diào)用 socket()、connect()之后就 會向 TCP 服務(wù)器發(fā)送了一個(gè)連接請求。TCP 服務(wù)器監(jiān)聽到這個(gè)請求之后,就會調(diào)用 accept()函數(shù) 取接收請求,這樣連接就建立好了。
2. 主要函數(shù)
(1) socket()函數(shù)
socket 的函數(shù)原型如下所示:socket(int domain,int type,int protocol)。Socket()用于
創(chuàng)建一個(gè) socket 描述符(
socket descriptor),它唯一標(biāo)識一個(gè) socket。這個(gè) socket 描述符可
以作為參數(shù),通過它來進(jìn)行一些讀寫操作。socket 有三個(gè)參數(shù):domain、type 和 protocol。其
中,domain 即協(xié)議域,又稱 family。產(chǎn)用的協(xié)議域有: AF _INET、AF_INET6、AF_ LOCAL (或
稱 AF_ UNIX, Unix 域 socket)、AF ROUTE 等。協(xié)議域決定了 socket 的地址類型、在通信中必
須采用對應(yīng)的地址,例如 AFINET 決定了要用 ipv4 地址(32 位)與端口號(16 位)的組合,AFUNIX
決定了要用一個(gè)絕對路徑名作為地址。而type用來指定socket類型。常用的socket類型有: SOCK.
STREAM、SOCK DGRAM、SOCK_ RAW、SOCK_ PACKET、 SOCK_ SEQPACKET 等。
其中,SOCK STREAM 表示提供面向連接的穩(wěn)定數(shù)據(jù)傳輸,即 TCP 協(xié)議。SOCK DGRAM 表示
使用不連續(xù)、不可靠的數(shù)據(jù)包連接。最后,
protocol指定了 socket的協(xié)議。常用的協(xié)議有 IPPROTO_
TCP、IPPTOTO_ UDP、IPPROTO_SCTP、IPPROTO TIPC 等,它們分別對應(yīng) TCP 傳輸協(xié)議、
UDP 傳輸協(xié)議、STCP 傳輸協(xié)議、TIPC 傳輸協(xié)議。
當(dāng)調(diào)用 socket 創(chuàng)建一個(gè) socket 時(shí),返回的 socket 描述字它存在于協(xié)議域(address family,
AF_ XXX)空間中,但沒有一個(gè)具體的地址。如果想要給它賦予一個(gè)地址,就必須調(diào)用 bind()函數(shù),
否則系統(tǒng)就會在調(diào)用 connect()、listen() 時(shí)自動隨機(jī)分配一個(gè)端口。如果調(diào)用成功就返回新創(chuàng)建
的套接字的描述符,如果失敗就返回 INVALIDSOCKET( Linux 下失敗返回-1 )。套接字描述符是
一個(gè)整數(shù)類型的值。每個(gè)進(jìn)程的進(jìn)程空間里都有一個(gè)套接字描述符表,該表中存放著套接字描述符
和套接字?jǐn)?shù)據(jù)結(jié)構(gòu)的對應(yīng)關(guān)系。套接字?jǐn)?shù)據(jù)結(jié)構(gòu)都存放在操作系統(tǒng)的內(nèi)核緩沖里。
(2) bind()函數(shù)
bind()的函數(shù)原型是:int bind(
int sockfd, const struct sockaddr *addr, socklen_t
addrlen) 。
bind()函數(shù)把一個(gè)地址域中的特定地址賦給 socket。例如對應(yīng) AF INET,
AF_INETT6
就是把一個(gè) ipv4 或 ipv6 地址和端口號組合賦給 socket。函數(shù)的有個(gè)參數(shù):sockfd、addr 和
addrlen。其中 sockfd 是 socket 描述字,它是通過 socket()函數(shù)創(chuàng)建來唯一標(biāo)識一個(gè) socket 的。
bind()函數(shù)就是將給這個(gè)描述字綁定一個(gè)名字。而 addr 描述了一個(gè) const struct sockaddr*指針,
指向要綁定給 sockfd 的協(xié)議地址。這個(gè)地址的結(jié)構(gòu)根據(jù)地址創(chuàng)建 socket 時(shí)的地址協(xié)議域的不同
而不同,ipv4 和 ipv6 分別對應(yīng)不同的代碼。例如在本實(shí)驗(yàn)中采用 ipv4 協(xié)議,對應(yīng)代碼如下:
servaddr.sin_family = AF_INET;
//地址域(指定地址格式) ,設(shè)為 AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //AF_INET:所有 IP 都可以連上
servaddr.sin_port = htons(6666);
//端口號
最后,addrlen 對應(yīng)的是地址的長度。通常服務(wù)器在啟動的時(shí)候都會綁定一個(gè)眾所周知的地址
(如 ip 地址+端口號)。用于提供服務(wù)??蛻艟涂梢酝ㄟ^它來連接服務(wù)器,而客戶端就不用指定,有
系統(tǒng)自動分配一個(gè)端口號和自身的 IP 地址組合。這就是為什么通常服務(wù)器端在調(diào)用 listen 之前會
調(diào)用 bind(),而客戶端就不用調(diào)用,而是在 connect()時(shí)由系統(tǒng)隨機(jī)生成一個(gè)。返回值:如果函數(shù)執(zhí)
行成功,返回值為 0,反之為 SOCKET ERROR。
(3) listen()函數(shù)和 connect() 函數(shù)
listen()和 connect()函數(shù)的原型是:int listen(int sockfd , int backlog)和 int connect(int
sockfd , const struct sockaddr *addr, socklen_t addr len)。服務(wù)器在調(diào)用 socket()和 bind()
函數(shù)之后就會調(diào)用 listen()函數(shù)來監(jiān)聽這個(gè) socket,如果客戶端這時(shí)調(diào)用 connect()發(fā)出連接請求,
服務(wù)器端就會接收到這個(gè)請求從而建立連接。
listen()函數(shù)的第一個(gè)參數(shù)即為要監(jiān)聽的 socket 描述字,第二個(gè)參數(shù)為相應(yīng) socket 可以排隊(duì)
的最大連接個(gè)數(shù)。socket()函數(shù)創(chuàng)建的 socket 默認(rèn)是一個(gè)主動類型的,listen 函數(shù)將 socket 變?yōu)?
被動類型的,等待客戶的連接請求。connect()函數(shù)的第一個(gè)參數(shù)即為客戶端的 socket 描述字,第
二參數(shù)為服務(wù)器的 socket 地址,第三個(gè)參數(shù)為 socket 地址的長度??蛻舳送ㄟ^調(diào)用 connect()
函數(shù)來建立與 TCP 服務(wù)器的連接。
(4) accept()函數(shù)
建立好連接后,就可以開始網(wǎng)絡(luò) I/0 操作了,accept 的函數(shù)原型是:int accept(int sockfd,
struct sockaddr *addr, socklen_t *addrl en)。accept 函數(shù)的第一個(gè)參數(shù)為服務(wù)器的 socket 描
述字;第二個(gè)參數(shù)為指向 struct sockaddr*的指針,用于返回客戶端的協(xié)議地址;第三個(gè)參數(shù)為
協(xié)議地址的長度。如果 accpet 成功,那么其返回值是由內(nèi)核自動生成的一個(gè)全新的描述字,代表
與返回客戶的 TCP 連接。一個(gè)服務(wù)器通常僅僅只創(chuàng)建一個(gè)監(jiān)聽 socket 描述字,它在該服務(wù)器的生
命周期內(nèi)一直存在。內(nèi)核為每個(gè)由服務(wù)器進(jìn)程接受的客戶創(chuàng)建了一個(gè)已連接 socket 描述字,當(dāng)服
務(wù)器完成了對某個(gè)客戶的服務(wù),相應(yīng)的已連接 socket 描述字就被關(guān)閉。
(5) read() 和 write() 函數(shù)
至此服務(wù)器與客戶已經(jīng)建立好連接了,可以調(diào)用網(wǎng)絡(luò) I/0 進(jìn)行讀寫操作了,即實(shí)現(xiàn)了網(wǎng)絡(luò)中
不同進(jìn)程之間的通信。網(wǎng)絡(luò) I/0 操作有下面幾組:
read()/write();
recv()/send();
readv()/writev();
recvmsg()/sendmsg();recvfom()/sendto()。
最常用的則是 read()和 write()。read() 的函數(shù)原型是:ssize_ t read(int fd, void *buf, size_ t
count)read()函數(shù)是負(fù)責(zé)從 fd 中讀取內(nèi)容。當(dāng)讀取成功時(shí),read()返回實(shí)際所讀的字節(jié)數(shù),如果返
回的值是 0 表示已經(jīng)讀到文件的結(jié)束了,小于 0 表示出現(xiàn)了錯誤。如果錯誤為 EINTR 說明讀是由
中斷引起的,如果是 ECONNREST 表示網(wǎng)絡(luò)連接出了問題。三個(gè)參數(shù)分別是代表 socket 描述字
(fd);緩沖區(qū)(buf);緩沖區(qū)長度(count)。write()的函數(shù)原型是:ssize_ t write(int fd, const
void *buf, size_ t count)。write()函數(shù)將緩沖區(qū)中的 nbytes 字節(jié)內(nèi)容寫人文件描述符 fd 成功時(shí)
返回寫的字節(jié)數(shù)。失敗時(shí)返回-1,并設(shè)置 errmo 變量。在網(wǎng)絡(luò)程序中,當(dāng)我們向套接字文件描述
符寫時(shí)有兩種可能:①write 的返回值大于 0,表示寫了部分或者是全部的數(shù)據(jù);②返回的值小于 0,
此時(shí)出現(xiàn)了錯誤。實(shí)際中要根據(jù)錯誤類型來處理。如果錯誤為 EINTR 表示在寫的時(shí)候出現(xiàn)了中斷
錯誤。如果為 EPIPE 表示網(wǎng)絡(luò)連接出現(xiàn)了問題(對方已經(jīng)關(guān)閉了連接)。
(6) close()函數(shù)
完成了讀寫操作就要關(guān)閉相應(yīng)的socket 描述字。
close 的函數(shù)原型是:int close (int fd)。close 操作一般使相應(yīng) socket 描述字的引用計(jì)數(shù)值為-1,只有當(dāng)引用計(jì)數(shù)為 0 的時(shí)候,才會觸發(fā) TCP
客戶端向服務(wù)器發(fā)送終止連接請求。
3. 代碼實(shí)現(xiàn)
(1) 服務(wù)器端
首先建立一個(gè) socket 描述符,設(shè)置套接字的類型為:SOCK_STREAM,即表示提供面向連接 的穩(wěn)定數(shù)據(jù)傳輸,即 TCP 協(xié)議。并把一個(gè)地址域中的特定地址賦給 socket。對應(yīng) AF INET,即 把一個(gè) ipv4 地址和端口號組合賦給 socket。對應(yīng)代碼:listenfd = socket(AF_INET,
SOCK_STREAM, 0。接著,設(shè)置可連接的 ip 為任意 ip 值:sin_addr.s_addr =
htonl(INADDR_ANY),設(shè)置端口號:sin_port = htons(6666)。然后,使用 bind()函數(shù)將 servaddr
地址綁定到該 socket 描述符:bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))
== -1,再用 listen()函數(shù)讓服務(wù)器處于監(jiān)聽模式等待客戶端的連接,并設(shè)置最大連接數(shù)。接著使用
while 循環(huán)接受客戶端的請求,在循環(huán)里用指向 client_addr 的指針來獲取對方的地址 connfd =
accept(listenfd, (struct sockaddr*)&client_addr, &size)) == -1,然后用 read()函數(shù)接收客戶
端發(fā)來的數(shù)據(jù)并重寫緩沖區(qū)。最后結(jié)束連接,關(guān)閉套接字。
(2) 客戶端
首先創(chuàng)建套接字并設(shè)置相關(guān)的協(xié)議,地址域、端口號及地址格式,并設(shè)置在終端獲取客戶端所
要連接的 ip 地址。然后采用 inet_pton()函數(shù)將字符串轉(zhuǎn)換成網(wǎng)絡(luò)地址,并復(fù)制到服務(wù)器的
sin_addr 中:inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0。接著用 connnect()
函數(shù)將兩者連接起來,連接成功后,讀取文件內(nèi)容(/home/kkk/桌面/tt.jpg),發(fā)送給服務(wù)器端。
服務(wù)器端新建 tt.jpg 文件,將接收到的文件內(nèi)容保存到 tt.jpg 中,并設(shè)置 tt.png 位于當(dāng)前目錄下。
最后完成連接,關(guān)閉套接字。
client.cpp為客戶端實(shí)現(xiàn)代碼
sever.cpp為服務(wù)器端實(shí)現(xiàn)代碼
執(zhí)行make命令后,生成server和client兩個(gè)可執(zhí)行文件。分別打開兩個(gè)終端窗口,分別執(zhí)行./server命令和./client 127.0.0.1命令,表示連上本機(jī)的6666端口,./server命令的首先執(zhí)行。執(zhí)行./client 127.0.0.1命令后,client客戶端執(zhí)行完畢直接退出,這時(shí)server的終端窗口輸出“recv msg from client:”。打開server.cpp文件所在的目錄,可看到tt.jpg文件已經(jīng)生成。
client.cpp
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int sockfd, len;
char buffer[MAXLINE];
struct sockaddr_in servaddr;
FILE *fq;
//創(chuàng)建套接字,打印客戶端所要連接的服務(wù)器ip地址:127.0.0.1
if( argc != 2){
printf("usage: ./client <ipaddress>\n");
return 0;
}
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
return 0;
}
//設(shè)置遠(yuǎn)程地址信息
memset(&servaddr, 0, sizeof(servaddr)); //先把servaddr地址清空,再復(fù)制
servaddr.sin_family = AF_INET; //地址族(指定地址格式) ,設(shè)為AF_INET
servaddr.sin_port = htons(6666); //連接服務(wù)器端口號
// arg[1]是寫ip地址的地方,inet_pton 是 IP 地址轉(zhuǎn)換函數(shù),可以在將 IP 地址在“點(diǎn)分十進(jìn)制”和“二進(jìn)制整數(shù)”之間轉(zhuǎn)換
//inet_pton函數(shù)的原型是:int inet_pton(int af,const char *src,void *dst)
//這個(gè)函數(shù)將字符串轉(zhuǎn)換成網(wǎng)絡(luò)地址,第一個(gè)參數(shù)af=AF_INET,,src指向ASCII的地址的首地址(x.x.x.x的格式)
//函數(shù)將該地址轉(zhuǎn)換成in_addr的結(jié)構(gòu)體并復(fù)制到*dst,即sin_addr中
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
return 0;
}
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno); //連接失敗響應(yīng)函數(shù)
return 0;
}
//連接成功后,讀取文件內(nèi)容(/home/kkk/桌面/tt.jpg),發(fā)送給服務(wù)器端
if( ( fq = fopen("/home/kkk/桌面/tt.jpg","rb") ) == NULL ){
printf("File open.\n");
close(sockfd);
exit(1);
}
bzero(buffer,sizeof(buffer));
//服務(wù)器端新建tt.png文件,將接收到的文件內(nèi)容保存到tt.png中,tt.jpg在當(dāng)前目錄下;
while(!feof(fq)){
len = fread(buffer, 1, sizeof(buffer), fq);
if(len != write(sockfd, buffer, len)){
printf("write.\n");
break;
}
}
close(sockfd); // 關(guān)閉套節(jié)字
fclose(fq);
return 0;
}
sever.cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#define MAXLINE 4096
int main(int argc, char** argv){
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[4096];
FILE *fp;
int n;
//指定套接字的類型: SOCK_STREAM(即TCP協(xié)議,一種可靠的、雙向的通信數(shù)據(jù)流,數(shù)據(jù)可以準(zhǔn)確無誤地到達(dá)另一臺計(jì)算機(jī),如果損壞或丟失,可以重新發(fā)送。)
//建立一個(gè)socket描述符,socket(ipv4,提供面向連接的穩(wěn)定傳輸數(shù)據(jù),指定協(xié)議為0),AFINET決定了要用ipv4地址(32位)與端口號(16位)的組合
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0; //如果連接錯誤,打印錯誤提示
}
printf("----init socket----\n"); //初始化套接字
memset(&servaddr, 0, sizeof(servaddr)); //先把servaddr地址清空,再復(fù)制
servaddr.sin_family = AF_INET; //套接字使用的地址族(指定地址格式):ipv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //AF_INET表示什么IP都可以連上
servaddr.sin_port = htons(6666); //端口號6666,1024 ~ 49151:普通用戶注冊的端口號
//設(shè)置端口可重用
int contain;
setsockopt(listenfd,SOL_SOCKET, SO_REUSEADDR, &contain, sizeof(int));
將servaddr地址綁定到該socket描述符
if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
printf("----bind sucess----\n");
//進(jìn)入監(jiān)聽模式,監(jiān)聽這個(gè)socket描述符,10這里指的是監(jiān)聽隊(duì)列中允許保持的尚未處理的最大連接數(shù)是10
if( listen(listenfd, 10) == -1){
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}
if((fp = fopen("tt.jpg","ab") ) == NULL )
{
printf("File.\n");
close(listenfd);
exit(1);
}
printf("======waiting for client's request======\n");
// 循環(huán)接受客戶的連接請求
while(1){
struct sockaddr_in client_addr;
socklen_t size=sizeof(client_addr);
//一個(gè)指向client_addr的指針用于獲取對方的地址
if( (connfd = accept(listenfd, (struct sockaddr*)&client_addr, &size)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
//從客戶端接收數(shù)據(jù)
while(1){
n = read(connfd, buff, MAXLINE); //recv 函數(shù)接收到的字符串是不帶 ”\0”結(jié)束符的
if(n == 0)
break;
fwrite(buff, 1, n, fp);
}
buff[n] = '\0';
printf("recv msg from client: %s\n", buff); 要用 printf輸出時(shí)必須得先加上結(jié)束符”\0"
close(connfd);
fclose(fp);
}
/*在 while 循環(huán)里持續(xù)接收包,注意, accept 和 read 是都是在 while 循環(huán)里的,
也就是收到包之后,listenfd就沒用了,并關(guān)閉它,下一個(gè)包重新接收包 。
*/
close(listenfd); //關(guān)閉監(jiān)聽套節(jié)字
return 0;
}
運(yùn)行結(jié)果:

參考文章:
Linux C/C++ TCP Socket傳輸文件或圖片實(shí)例 - zkfopen - 博客園
Socket原理及實(shí)踐(Java/C/C++) - xiuzhublog - 博客園文章來源:http://www.zghlxwxcb.cn/news/detail-644956.html
?Linux C/C++ TCP Socket通信實(shí)例 - zkfopen - 博客園文章來源地址http://www.zghlxwxcb.cn/news/detail-644956.html
到了這里,關(guān)于TCP通信實(shí)現(xiàn)客戶端向服務(wù)器發(fā)送圖片的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!