網路程式設計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
#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*。