我們假設在兩個不同的區域網路後面分别有2台客戶機A和 B,AB所在的區域網路都分别通過一個路由器接入網際網路。網際網路上有一台伺服器S。
現在AB是無法直接和對方發送資訊的,AB都不知道對方在網際網路上真正的IP和端口, AB所在的區域網路的路由器隻允許内部向外主動發送的資訊通過。對于B直接發送給A的路由器的消息,路由會認為其“不被信任”而直接丢棄。
要實作 AB直接的通訊,就必須進行以下3步:A首先連接配接網際網路上的伺服器S并發送一條消息(對于UDP這種無連接配接的協定其實直接初始會話發送消息即可),這樣S就擷取了A在網際網路上的實際終端(發送消息的IP和端口号)。接着 B也進行同樣的步驟,S就知道了AB在網際網路上的終端(這就是“打洞”)。接着S分别告訴A和B對方用戶端在網際網路上的實際終端,也即S告訴A客戶B的會話終端,S告訴B客戶A的會話終端。這樣,在AB都知道了對方的實際終端之後,就可以直接通過實際終端發送消息了(因為先前雙方都向外發送過消息,路由上已經有允許資料進出的消息通道)。
1:啟動伺服器,監聽端口8877
2:第一次啟動用戶端(稱為client1),連上伺服器,伺服器将傳回字元串first,辨別這個是client1,同時,伺服器将記錄下這個用戶端的(經過轉換之後的)IP和端口。
3:第二次啟動用戶端(稱為client2),連上伺服器,伺服器将向其傳回自身的發送端口(稱為port2),以及client1的(經過轉換之後的)IP和端口。
4:然後伺服器再發client1傳回client2(經過轉換之後的)IP和端口,然後斷開與這兩個用戶端的連接配接(此時,伺服器的工作已經全部完成了)
5:client2嘗試連接配接client1,這次肯定會失敗,但它會在路由器上留下記錄,以幫忙client1成功穿透,連接配接上自己,然後設定port2端口為可重用端口,并監聽端口port2。
6:client1嘗試去連接配接client2,前幾次可能會失敗,因為穿透還沒成功,如果連接配接10次都失敗,就證明穿透失敗了(可能是硬體不支援),如果成功,則每秒向client2發送一次hello, world
7:如果client1不斷出現send message: Hello, world,client2不斷出現recv message: Hello, world,則證明實驗成功了,否則就是失敗了。
1:這個程式隻是一個DEMO,是以肯定有很多不完善的地方,請大家多多見諒。
2:在很多網絡中,這個程式并不能打洞成功,可能是硬體的問題(畢竟不是每種路由器都支援穿透),也可能是我程式的問題,如果大家有意見或建議,歡迎留言或給我發郵件(郵箱是:[email protected])
伺服器端:

/*
檔案:server.c
PS:第一個連接配接上伺服器的用戶端,稱為client1,第二個連接配接上伺服器的用戶端稱為client2
這個伺服器的功能是:
1:對于client1,它傳回"first",并在client2連接配接上之後,将client2經過轉換後的IP和port發給client1;
2:對于client2,它傳回client1經過轉換後的IP和port和自身的port,并在随後斷開與他們的連接配接。
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#define MAXLINE 128
#define SERV_PORT 8877
//發生了緻命錯誤,退出程式
void error_quit(const char *str)
{
fprintf(stderr, "%s", str);
//如果設定了錯誤号,就輸入出錯原因
if( errno != 0 )
fprintf(stderr, " : %s", strerror(errno));
printf("\n");
exit(1);
}
int main(void)
{
int i, res, cur_port;
int connfd, firstfd, listenfd;
int count = 0;
char str_ip[MAXLINE]; //緩存IP位址
char cur_inf[MAXLINE]; //目前的連接配接資訊[IP+port]
char first_inf[MAXLINE]; //第一個連結的資訊[IP+port]
char buffer[MAXLINE]; //臨時發送緩沖區
socklen_t clilen;
struct sockaddr_in cliaddr;
struct sockaddr_in servaddr;
//建立用于監聽TCP協定套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//把socket和socket位址結構聯系起來
res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if( -1 == res )
error_quit("bind error");
//開始監聽端口
res = listen(listenfd, INADDR_ANY);
error_quit("listen error");
while( 1 )
{
//接收來自用戶端的連接配接
connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);
if( -1 == connfd )
error_quit("accept error");
inet_ntop(AF_INET, (void*)&cliaddr.sin_addr, str_ip, sizeof(str_ip));
count++;
//對于第一個連結,将其的IP+port存儲到first_inf中,
//并和它建立長連結,然後向它發送字元串'first',
if( count == 1 )
{
firstfd = connfd;
cur_port = ntohs(cliaddr.sin_port);
snprintf(first_inf, MAXLINE, "%s %d", str_ip, cur_port);
strcpy(cur_inf, "first\n");
write(connfd, cur_inf, strlen(cur_inf)+1);
}
//對于第二個連結,将其的IP+port發送給第一個連結,
//将第一個連結的資訊和他自身的port傳回給它自己,
//然後斷開兩個連結,并重置計數器
else if( count == 2 )
snprintf(cur_inf, MAXLINE, "%s %d\n", str_ip, cur_port);
snprintf(buffer, MAXLINE, "%s %d\n", first_inf, cur_port);
write(connfd, buffer, strlen(buffer)+1);
write(firstfd, cur_inf, strlen(cur_inf)+1);
close(connfd);
close(firstfd);
count = 0;
//如果程式運作到這裡,那肯定是出錯了
else
error_quit("Bad required");
}
return 0;
}
用戶端:

檔案:client.c
這個程式的功能是:先連接配接上伺服器,根據伺服器的傳回決定它是client1還是client2,
若是client1,它就從伺服器上得到client2的IP和Port,連接配接上client2,
若是client2,它就從伺服器上得到client1的IP和Port和自身經轉換後的port,
在嘗試連接配接了一下client1後(這個操作會失敗),然後根據伺服器傳回的port進行監聽。
這樣以後,就能在兩個用戶端之間進行點對點通信了。
typedef struct
{
char ip[32];
int port;
}server;
fprintf(stderr, "%s", str);
int main(int argc, char **argv)
int i, res, port;
int connfd, sockfd, listenfd;
unsigned int value = 1;
char buffer[MAXLINE];
socklen_t clilen;
struct sockaddr_in servaddr, sockaddr, connaddr;
server other;
if( argc != 2 )
error_quit("Using: ./client <IP Address>");
//建立用于連結(主伺服器)的套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &sockaddr.sin_addr);
//設定端口可以被重用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
//連接配接主伺服器
res = connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
if( res < 0 )
error_quit("connect error");
//從主伺服器中讀取出資訊
res = read(sockfd, buffer, MAXLINE);
error_quit("read error");
printf("Get: %s", buffer);
//若伺服器傳回的是first,則證明是第一個用戶端
if( 'f' == buffer[0] )
//從伺服器中讀取第二個用戶端的IP+port
res = read(sockfd, buffer, MAXLINE);
sscanf(buffer, "%s %d", other.ip, &other.port);
printf("ff: %s %d\n", other.ip, other.port);
//建立用于的套接字
connfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&connaddr, 0, sizeof(connaddr));
connaddr.sin_family = AF_INET;
connaddr.sin_addr.s_addr = htonl(INADDR_ANY);
connaddr.sin_port = htons(other.port);
inet_pton(AF_INET, other.ip, &connaddr.sin_addr);
//嘗試去連接配接第二個用戶端,前幾次可能會失敗,因為穿透還沒成功,
//如果連接配接10次都失敗,就證明穿透失敗了(可能是硬體不支援)
while( 1 )
static int j = 1;
res = connect(connfd, (struct sockaddr *)&connaddr, sizeof(connaddr));
if( res == -1 )
{
if( j >= 10 )
error_quit("can't connect to the other client\n");
printf("connect error, try again. %d\n", j++);
sleep(1);
}
else
break;
strcpy(buffer, "Hello, world\n");
//連接配接成功後,每隔一秒鐘向對方(用戶端2)發送一句hello, world
res = write(connfd, buffer, strlen(buffer)+1);
if( res <= 0 )
error_quit("write error");
printf("send message: %s", buffer);
sleep(1);
//第二個用戶端的行為
else
//從主伺服器傳回的資訊中取出用戶端1的IP+port和自己公網映射後的port
sscanf(buffer, "%s %d %d", other.ip, &other.port, &port);
//建立用于TCP協定的套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
connaddr.sin_port = htons(other.port);
//設定端口重用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
//嘗試連接配接用戶端1,肯定會失敗,但它會在路由器上留下記錄,
//以幫忙用戶端1成功穿透,連接配接上自己
res = connect(sockfd, (struct sockaddr *)&connaddr, sizeof(connaddr));
if( res < 0 )
printf("connect error\n");
//建立用于監聽的套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
//把socket和socket位址結構聯系起來
res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if( -1 == res )
error_quit("bind error");
//開始監聽端口
res = listen(listenfd, INADDR_ANY);
error_quit("listen error");
//接收來自用戶端1的連接配接
connfd = accept(listenfd,(struct sockaddr *)&sockaddr, &clilen);
if( -1 == connfd )
error_quit("accept error");
while( 1 )
//循環讀取來自于用戶端1的資訊
res = read(connfd, buffer, MAXLINE);
if( res <= 0 )
error_quit("read error");
printf("recv message: %s", buffer);
(第一個終端)
qch@qch ~/program/tcode $ gcc server.c -o server
qch@qch ~/program/tcode $ ./server &
[1] 4688
qch@qch ~/program/tcode $ gcc client.c -o client
qch@qch ~/program/tcode $ ./client localhost
Get: first
ff: 127.0.0.1 38052
send message: Hello, world
.................
第二個終端:
Get: 127.0.0.1 38073 38074
connect error
recv message: Hello, world
..................