1. 程式人生 > >網路程式設計3-TCP/UDP

網路程式設計3-TCP/UDP

4.2 Socket程式設計

基礎知識:

1.在windows/linux下都有一個ping命令,用來檢查對方主機是否連通。若請求超時:對方關機或離線;己方沒有連網;對方線上,但遮蔽了ping服務。

2.檢視自己的IP,windows:ipconfig;linux:ifconfig。

3.抓包軟體:Wireshark。注意:當ip為127.0.0.1時,無法抓包,因為這是迴環地址,資料不經過網絡卡。

4.2.1 TCP

//伺服器端

#include <iostream>

#include <winsock2.h>

#include

 <windows.h>

#pragma comment(lib,"ws2_32.lib")//引用庫檔案

using namespace std;

 

int main(int argc, char ** argv)

{

//步驟1:當前應用程式和相應的socket庫繫結

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD(2, 2);

err = WSAStartup(wVersionRequested, &wsaData);

if (err != 0)

{

cout << "WSAStartup Failed!" << endl;

return -1;

}

 

//步驟2:建立TCP網路套接字

SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);

//步驟3:設定伺服器端地址結構SOCKADDR_IN

SOCKADDR_IN addr_server;

//3.1 初始化SOCKADDR_IN

memset(&addr_server, 0, sizeof(addr_server));

//3.2 設定地址協議族

addr_server.sin_family = AF_INET;

//3.3 設定客戶端可連線的IP

addr_server.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示繫結電腦上所有網絡卡IP

//3.4 設定客戶端可連線的埠

addr_server.sin_port = htons(1900);//不能使用公認埠,即埠>= 1024

 

//步驟4:將套接字和地址繫結

bind(sockServer, (SOCKADDR*)&addr_server, sizeof(addr_server));

 

//步驟5:監聽

listen(sockServer, 5);

cout << "start listen..." << endl;

 

SOCKADDR_IN addr_client;

int len = sizeof(SOCKADDR);

 

while (1)

{

//步驟6:與客戶端建立連線

SOCKET sockClient = accept(sockServer, (SOCKADDR*)&addr_client, &len);

 

char hostname[108];

if (gethostname(hostname, 108) != 0) //獲得主機名

strcpy(hostname, "Get Hostname Failed!");

printf("Welecome %s Connected to %s!\n", inet_ntoa(addr_client.sin_addr), hostname);

 

//步驟7:從客戶端讀取資料,向客戶端傳送資料

char Buf[1024] = "\0";

recv(sockClient, Buf, 1024, 0);

cout << Buf << endl;

strcat(Buf, ":Server Received");

send(sockClient, Buf, strlen(Buf) + 1, 0);

//步驟8.1:關閉套接字

closesocket(sockClient);

}

//步驟8.2:關閉套接字

closesocket(sockServer);

//步驟9:將應用程式和socket庫解除繫結

WSACleanup();

return 0;

}

//客戶端

#include <iostream>

#include <winsock2.h>

#include <windows.h>

#pragma comment(lib,"ws2_32.lib")//引用庫檔案

using namespace std;

 

int main(int argc, char ** argv)

{

//步驟1:當前應用程式和相應的socket庫繫結

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD(2, 2);

err = WSAStartup(wVersionRequested, &wsaData);

if (err != 0)

{

cout << "WSAStartup Failed!" << endl;

return -1;

}

 

//步驟2:建立TCP網路套接字

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

//步驟3:設定伺服器端地址結構SOCKADDR_IN

SOCKADDR_IN addr_server;

//3.1 初始化SOCKADDR_IN

memset(&addr_server, 0, sizeof(addr_server));

//3.2 設定地址協議族

addr_server.sin_family = AF_INET;

//3.3 設定伺服器端的IP

addr_server.sin_addr.s_addr = inet_addr("192.168.1.101");

//3.4 設定伺服器端的埠號

addr_server.sin_port = htons(1900);//不能使用公認埠,即埠>= 1024

 

//步驟4:客戶端連線伺服器

int result = connect(sock, (SOCKADDR*)&addr_server, sizeof(addr_server));

if (result == -1)

return -1;

cout << "Connect Seccessed!" << endl;

 

//步驟5:向伺服器端傳送資料

char Buf[1024] = "\0";

cin.getline(Buf, 1024);

send(sock, Buf, 1024, 0);

recv(sock, Buf, 1024, 0);

printf("%s\n", Buf);

 

//步驟6:關閉套接字

closesocket(sock);

//步驟7:將應用程式和socket庫解除繫結

WSACleanup();

return 0;

}

  TCP協議適合區域網傳輸。以上程式碼中,客戶端只能向伺服器端傳送一次資料;伺服器端可以連線多個客戶端,但只能同時處理一個客戶端傳來的資料。

知識點1:

WSAStartup(引數1,引數2):初始化程序呼叫的Winsock相關的dll,即Ws2_32.dll。

引數1:標識了使用者呼叫的Winsock的版本號,高位元組指明輔版本編號,低位元組指明主版本編號。通常使用MAKEWORD來生成一個版本號,當前Winsock sockets的版本號為2.2,用到的dll是 Ws2_32.dll。

引數2:指向WSADATA結構體的指標,它返回系統對Windows Sockets的描述。

WSACleanup():釋放對WINsock連結庫的呼叫。

知識點2:

socket(引數1,引數2,引數3):建立指定傳輸服務的socket。

引數1:指明地址簇型別,常用的地址簇如下:AF_UNSPEC(未指明)、AF_INET(IPv4)、AF_NETBIOS(NETBIOS地址簇)、AF_INET6(IPv6)、AF_IRDA(Infrared Data Association (IrDA)地址簇)、AF_BTM(Bluetooth)。

引數2:指明socket的型別,Windows Sockets 2常見型別如下:SOCK_STREAM(流套接字,使用TCP協議)、SOCK_DGRAM(資料報套接字,使用UDP協議)、SOCK_RAW(原始套接字)、SOCK_RDM(提供可靠的訊息資料報文)、SOCK_SEQPACKET(在UDP的基礎上提供了偽流資料包)。

引數3:指明資料傳輸協議。常使用的協議如下:IPPROTO_TCP(TCP協議,使用條件:引數1是AF_INET or AF_INET6、引數2是SOCK_STREAM)、IPPROTO_UDP(UDP協議,使用條件:引數1是AF_INET or AF_INET6、引數2是SOCK_DGRAM)、IPPROTO_RM(PGM協議(實際通用組播協議),使用條件:引數1是AF_INET、引數2是SOCK_RDM)。該引數取決去引數1和引數2的值,若引數1和引數2確定了資料傳輸協議,則引數3可為0。

知識點3:

sockaddr結構體:

typedef struct sockaddr {

ADDRESS_FAMILY sa_family;           //地址族

    CHAR sa_data[14];                   //14位元組,包含套接字中的目標地址和埠號

} SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;

sockaddr_in結構體:

typedef struct sockaddr_in {

ADDRESS_FAMILY sin_family;   //地址族

    USHORT sin_port;    //16位TCP/UDP埠號

    IN_ADDR sin_addr;   //32位IP地址

    CHAR sin_zero[8];   //不使用

} SOCKADDR_IN, *PSOCKADDR_IN;

sockaddr的缺陷:sa_data把目標地址和埠號混在一起。sockaddr_in解決了sockaddr的缺陷,把sin_port和sin_addr分開儲存。由於2個結構體都是16位元組,因此可以相互轉化。sockaddr常用於bind、connect、recvfrom、sendto等函式的引數,指明地址資訊,是一種通用的套接字地址。

知識點4:

主機位元組序:分為小端和大端。小端:將低位元組儲存在起始地址;大端:將高位元組儲存在起始地址。如對於資料0x12 34 56 78,小端儲存是0x78 56 34 12,大端儲存是0x12 34 56 78。

使用小端儲存資料的CPU:Intel x86和ARM;使用大端儲存資料的CPU:Power PC、MIPS UNIX和HP-PA UNIX。

網路位元組序是表示在網路傳輸中的位元組序,按照TCP/IP協議是按照大端方式傳輸。

C/C++常用的4個函式(僅在小端系統中生效):

htons:把unsigned short型別從主機序(小端)轉成網路位元組序(大端);

ntohs:把unsigned short型別從網路位元組序轉成主機序;

htonl:把unsigned long型別從主機序轉成網路位元組序;

ntohl :把unsigned long型別從網路位元組序轉成主機序。

在將地址和套接字繫結時,最好把地址轉換為網路位元組序再繫結,防止系統是大端而導致程式出現問題。

知識點5:

accept()用於伺服器端,connect()用於客戶端。這2個函式成對使用,TCP中的3次握手就由這個函式實現。

知識點6:

send()、recv():傳送和接受資料,用於TCP協議中。引數1使用accept返回的socket、connect中的socket。

4.2.2 UDP

//伺服器端

#include <iostream>

#include <winsock2.h>

#include <windows.h>

#pragma comment(lib,"ws2_32.lib")//引用庫檔案

using namespace std;

 

int main(int argc, char ** argv)

{

//步驟1:當前應用程式和相應的socket庫繫結

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD(2, 2);

err = WSAStartup(wVersionRequested, &wsaData);

if (err != 0)

{

cout << "WSAStartup Failed!" << endl;

return -1;

}

 

//步驟2:建立UDP網路套接字

SOCKET sockServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

//步驟3:設定伺服器端地址結構SOCKADDR_IN

SOCKADDR_IN addr_server;

//3.1 初始化SOCKADDR_IN

memset(&addr_server, 0, sizeof(addr_server));

//3.2 設定地址協議族

addr_server.sin_family = AF_INET;

//3.3 設定客戶端可連線的IP

addr_server.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示繫結電腦上所有網絡卡IP

//3.4 設定客戶端可連線的埠

addr_server.sin_port = htons(6000);//不能使用公認埠,即埠>= 1024

 

//步驟4:將套接字和地址繫結

bind(sockServer, (SOCKADDR*)&addr_server, sizeof(addr_server));

cout << "Start Server..." << endl;

 

while (1)

{

//步驟5:從客戶端讀取資料,向客戶端傳送資料

sockaddr_in addr_client;

memset(&addr_client, 0, sizeof(sockaddr_in));

int len = sizeof(SOCKADDR);

char Buf[1024] = "\0";

recvfrom(sockServer, Buf, 1024, 0, (sockaddr*)&addr_client, &len);

printf(

"Recv msg:%s from IP:[%s] Port:[%d]\n",

Buf,     

inet_ntoa(addr_client.sin_addr),

ntohs(addr_client.sin_port)

);

strcat(Buf, ":Server Received");

sendto(sockServer, Buf, strlen(Buf) + 1, 0, (SOCKADDR*)&addr_client, len);

}

//步驟6:關閉套接字

closesocket(sockServer);

//步驟7:將應用程式和socket庫解除繫結

WSACleanup();

return 0;

}

//客戶端

#include <iostream>

#include <winsock2.h>

#include <windows.h>

#pragma comment(lib,"ws2_32.lib")//引用庫檔案

using namespace std;

 

int main(int argc, char ** argv)

{

//步驟1:當前應用程式和相應的socket庫繫結

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD(2, 2);

err = WSAStartup(wVersionRequested, &wsaData);

if (err != 0)

{

cout << "WSAStartup Failed!" << endl;

return -1;

}

 

//步驟2:建立UDP網路套接字

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

//步驟3:設定伺服器端地址結構SOCKADDR_IN

SOCKADDR_IN addr_server;

//3.1 初始化SOCKADDR_IN

memset(&addr_server, 0, sizeof(addr_server));

//3.2 設定地址協議族

addr_server.sin_family = AF_INET;

//3.3 設定伺服器端的IP

addr_server.sin_addr.s_addr = inet_addr("192.168.1.101");

//3.4 設定伺服器端的埠號

addr_server.sin_port = htons(6000);//不能使用公認埠,即埠>= 1024

 

while (1)

{

//步驟4:向伺服器端傳送資料

int len = sizeof(SOCKADDR);

char Buf[1024] = "\0";

cin.getline(Buf, 1024);

sendto(sock, Buf, 1024, 0, (SOCKADDR*)&addr_server, len);

recvfrom(sock, Buf, 1024, 0, (SOCKADDR*)&addr_server, &len);

printf("%s\n", Buf);

}

 

//步驟5:關閉套接字

closesocket(sock);

//步驟6:將應用程式和socket庫解除繫結

WSACleanup();

return 0;

}

使用TCP/UDP通訊時,一定要注意埠號有沒有被佔用。windows下檢視埠號是否被佔用:netstat -ano;檢視埠號對應的PID:netstat -aon|findstr "1900"。

sendto()、recvfrom():接受和傳送資料,用於UDP協議中。引數1使用本端的socket結構體;引數5使用對端的SOCKADDR*。