1.1 套接字
C語言網絡編程其實本質上也是多進程之間通過socket套接字進行通信,知識進程可能位於不同的服務器上,常用的TCP/IP協議有3種套接字類型,如下所示:
1.1.1 流套接字(SOCK_STREAM)
流套接字用於提供面向連接、可靠的數據傳輸服務,該服務保證數據能夠實現無差錯、無重復發送,並按照順序接受。流套接字之所以能偶實現可靠的數據服務,原因在於使用了TCP傳輸控制協議。
1.1.2 數據報套接字(SOCK_DGRAM)
數據包套接字提供了一種無連接的服務,該服務不能保證數據傳輸的可靠性,數據有可能在傳輸過程中丟失或者出現數據重復,且無法保證順序的接受數據。數據報套接字使用UDP進行數據傳輸。
1.1.3 原始套接字(SOCK_RAW)
原始套接字允許對較低層次的協議直接訪問,常用於檢驗新的協議實現,或者訪問現有服務中配置的新設備,因為器可以自如控制Window下的多種協議,能夠對網絡地城的傳輸機制進行控制,所以可以應用原始套接字來操縱網絡層和傳輸層應用。如:通過原始套接字接受發向本機的ICMP、IGMP,或者接受TCP/IP棧不能處理的IP包。
1.1.3 C語言套接字數據結構
套接字通常由三個參數構成:IP地址, 端口號、傳輸層協議。C語言進行套接字編程的時候,通常會使用sockaddr和sockaddr_in兩種數據類型,用於保存套接字信息。
struct sockaddr
{
// 地址族,2字節
unsigned short sa_family;
// 存放地址和端口
char sa_data[14];
}
struct sockaddr_in
{
// 地址族
short int sin_family;
// 端口號
unsigned short int sin_port;
// 地址
struct in_addr sin_addr;
// 8字節數組,全為0,該字節數組的作用是為了讓兩種數據結構大小相同而保留的空字節
unsigned char sin_zero[8];
}
對於sockaddr,大部分的情況下知識用於bind、connect、recvform、sendto等函數的參數,指明地址信息,在一般編程中,並不對此結構體直接操作,而是用sockaddr_in代替。
兩種數據結構中,地址族都占2個字節,常見的地址族AF_INET, AF_INET6, AF_LOCAL。這里要注意字節序的問題,建議使用以下函數來對端口和地址進行處理。
uint16_t htons(uint16_t bost16bit)
uint32_t htonl(uint32_t bost32bit)
uint16_t ntons(uint16_t net16bit)
uint32_t ntons(uint32_t net32bit)
1.2 基於TCP的網絡編程
客戶端和服務器的連接和三次握手發生在accept函數下,listen函數知識創建了socket的監聽模式。
使用socket進行TCP通信時,經常使用的函數如下表所示。
函數 | 作用 |
---|---|
socket | 用於建立一個socket連接 |
bind | 將socket與本機的一個端口綁定, 隨后可以在該端口監聽服務請求 |
connect | 面向連接的客戶程序使用connect函數來配置socket,並於遠程服務器建立一個連接 |
listen | 是socket處於被動監聽模式, 並為該socket建立一個輸入數據隊列,將到達服務器請求保存在此隊列中,直到程序處理他們 |
accept | 讓服務器接收客戶端的連接請求 |
close | 停止在該socket上的任何操作 |
send | 數據發送函數 |
recv | 數據接收函數 |
1.2.1 服務端實現
服務端程序流程如下:
- 使用socket()函數創建一個socket
- 使用bind()函數,綁定ip地址、端口等信息到socket上
- 使用listen()函數,設置允許的最大連接數
- 使用accept()函數,接收客戶端上來的連接
- 使用send()和recv()函數或read()和write()函數,收發數據
- 使用close()函數關閉連接
實現代碼:
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MAX_SIZE 512
#define PORT 3332
int main(void)
{
int sockfd;
int sock_fd;
int recvnum;
int addrlen = sizeof(struct sockaddr);
struct sockaddr_in my_addr;
struct sockaddr addr;
char buf[MAX_SIZE];
// 填充服務器端的數據,用於套接字綁定
bzero(&my_addr, sizeof(struct sockaddr_in));
my_addr.sin_family = AF_INET; // 設置為IPV4
my_addr.sin_port = htons(PORT); // 將端口號主機序轉換為網絡序
my_addr.sin_addr.s_addr = inet_addr("192.168.192.128"); // ip設置為192.168.192.128
// 創建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
printf("create socket error!\n");
exit(1);
}
// 綁定套接字
if (bind(sockfd, (struct sockaddr *)&my_addr, addrlen) < 0)
{
printf("bind error!\n");
exit(1);
}
// 監聽端口和ip,設置最大連接數為3
if (listen(sockfd, 3) < 0)
{
printf("listen error!\n");
exit(1);
}
// 建立服務器端和客戶端連接
sock_fd = accept(sockfd, &addr, &addrlen);
// 建立連接后,產生新的套接字
if (sock_fd < 0)
{
printf("accept error!\n");
exit(1);
}
// 接收數據
if ((recvnum = recv(sock_fd, (void *)buf, MAX_SIZE, 0)) < 0)
{
printf("recv error!\n");
exit(1);
}
buf[recvnum] = '\0';
printf("recv from client: %s\n", buf);
memset(buf, 0, MAX_SIZE);
// 關閉連接
close(sockfd);
close(sock_fd);
return 0;
}
1.2.2 客戶端實現
客戶端程序流程如下:
- 使用socket()函數,創建一個socket
- 設置要連接的服務端ip地址和端口等屬性
- 使用connect()函數,連接服務器端
- 使用send()和recv()函數或read()和write()函數,收發數據
- 使用close()函數關閉網絡連接
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define MAX_SIZE 512
#define PORT 3332
int main()
{
int sockfd;
int addrlen = sizeof(struct sockaddr);
char buf[MAX_SIZE];
struct sockaddr_in serv_addr;
// 填充服務器端數據
bzero(&serv_addr, sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = ntohs(PORT);
serv_addr.sin_addr.s_addr = inet_addr("192.168.192.128");
// 創建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
printf("create socket error!\n");
exit(1);
}
// 連接服務器端
if (connect(sockfd, (struct sockaddr *)&serv_addr, addrlen) < 0)
{
printf("connect error!\n");
exit(1);
}
// 發送數據到服務端
memset(buf, 0, MAX_SIZE);
printf("enter some text:");
scanf("%s", buf);
if (send(sockfd, (void *)buf, MAX_SIZE, 0) < 0)
{
printf("send error!\n");
exit(1);
}
// 關閉連接
close(sockfd);
return 0;
}