網絡應用隨處可見,表現的形式也各不相同,有趣的是,在不同的表現形式之下都是基於相同的編程模型,依賴於相同的編程接口,因此學習網絡還是比較保值的,因爲這麼多基礎設備還在運行着,基本機制也在短時間內很難改變。
網絡編程的知識很多,包括進程,線程,信號等等,同時需要讀者瞭解TCP/IP協議,本文假定讀者已經瞭解熟悉這些相關知識,如果沒有可能需要學習APUE,計算機網絡這些基礎知識了。本文的最終目標就是編寫一個小型服務器。
客戶端-服務器模型(C/S)是廣泛使用的一種模型,一個應用是有一個服務器進程和多個客戶端進程組成的,客戶端發起請求,服務器做出響應,具體如下圖:
認識到服務器和客戶端是進程,而非主機是很重要的,因爲網絡編程是基於套接字的,所謂的網絡通信也是一個主機的a進程和另一個主機的b進程進行通信,socket實際上也是進程通信,只不過是跨網絡而已。
服務器和客戶端通常運行在不同的主機上,中間還隔着異構的網絡,十分複雜,好在前輩們設計出一套機制,TCP/IP協議棧幫我們封裝這些細節,使我們感覺網絡通信和主機內的通信很相似,我們忽略那些網絡的細節,從編程的角度來理解如何進行網絡編程。
對於一個主機而言,網絡只是一個IO設備,用於數據接收和數據發送。具體來說,
從網絡接收的數據從網絡適配器經過IO總線到存儲器,發送數據則相反,從主存到網絡適配器。Linux下自然也符合這種特點,不過Linux具有更高層次的抽象,Linux的其中一個設計理念就是,一切皆文件。對於網絡設備,Linux其實把它當作文件,因此接收發送數據其實也就是讀寫文件的操作,下面我們要講socket編程就是基於此原理。
根據網絡的應用範圍和架構層級,可以分成三個部分:
1. 最底層 - Ethernet Segment
由若干主機(hosts)通過交換機(hub)連接,通常範圍是房間或一層樓,如下圖所示:
- 每個 Ethernet 適配器有一個唯一的 48 位的地址(也就是 MAC 地址),例如 00:16:ea:e3:54:e6
- 不同主機間發送的數據稱爲幀(frame)
- Hub會把每個端口發來的所有數據複製到其他的端口
- 所有的主機都可以看到所有的數據(注意安全問題)
2. 下一層 - Bridged Ethernet Segment
通常範圍是一層樓,通過不同的 bridge 來連接不同的 ethernet segment。Bridge 知道從某端口出發可達的主機,並有選擇的在端口間複製數據。
爲了從概念上簡化,我們可以認爲,所有的 hub, bridge 可以抽象爲一條線,如下圖所示:
需要注意的是:之所以這麼簡化,是因爲無論是集線器hub,還是網橋brige實際上都在一個局域網,網橋只是起着擴大的作用,下面我們所講的路由器連接不同局域網。
3. 下一層 - internets
不同的(也許不兼容)的 LAN 可以通過 router (路由器)來進行物理上的連接,這樣連接起來的網絡稱爲 internet(注意是小寫,大寫的 Internet 可以認爲是最著名的 internet)
通過這一層的封裝,我們屏蔽不同異構網絡的差別,使他們看起來好像就是在一個局域網中通信,爲此,我們建立一套機制能夠讓某臺源主機通過這些不兼容的網絡發送數據到另一臺主機。而這也就是著名的IP協議幫我們完成的,簡單來說,它需要提供兩種基本能力:
- 命名機制
不同的局域網技術有不同和不兼容的方式來分配主機地址,IP通過定義一種統一的主機地址格式消除差異,每個主機路由器都被分配至少一個IP地址,這個地址唯一表示這個主機。
- 傳送機制
互聯網協議通過將數據捆綁成爲包的方式消除差異,一個包有包頭和有效載荷組成,其中包頭包括包的大小,源主機,目的主機地址等等,有效載荷就是要發送的數據。
IP地址
現在的網絡編程依舊還在應用C/S模型,而這個模型又遵從TCP/IP協議,因此,我們重點學習TCP/IP相關的內容。
IP地址是一個32位無符號整數,具體來說,它定義在如下的一個結構體:
struct in_addr
{
unsigned int s_addr;
};
1. 因爲主機可以有不同的字節序,TCP/IP定義了統一的網絡字節序–大端字節序。而我們的主機字節序可是小端也可能是大端,因此Linux下提供下面的函數可以實現網絡和主機字節序的轉換:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
2.
對於11_2題,先將字符串形式按照16進制讀入,用htonl保證大端字節序,然後轉化即可。
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("usage : [%s] [hex_number]\n", argv[0]);
exit(0);
}
// 0x8002c2f2
//
unsigned int addr;
sscanf(argv[1], "%x", &addr);
struct in_addr inaddr;
inaddr.s_addr = htonl(addr);//主機字節序->網絡字節序 大段字節序
printf("%s\n", inet_ntoa(inaddr));
return 0;
}
對於11_3題,利用inet _aton函數將點分十進制的字符串轉化爲 in _addr類型,同時需要考慮本機字節序。
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("usage ; [%s] [ip]\n", argv[1]);
exit(0);
}
struct in_addr in;
inet_aton(argv[1], &in);
unsigned int addr;
addr = ntohl(in.s_addr);//轉化爲主機字節序
printf("0x%x\n", addr);
return 0;
}
域名解析
183.232.231.172這是一個IP地址,具體來說,他就是百度的一個IP地址,是不是很難記呢?爲了便於人們記憶,我們利用Domain Naming System(DNS) 的概念,用來做 IP 地址到域名的映射,這樣子我們訪問網站只需要用域名即可,而不需要記憶那些數字了,我們可以利用nslookup命令查看~
套接字編程
客戶端和服務器通過連接(connection)來發送字節流,特點是:
- 點對點: 連接一對進程
- 全雙工: 數據同時可以在兩個方向流動
- 可靠: 字節的發送的順序和收到的一致
Socket 則可以認爲是 connection 的 endpoint,socket 地址是一個 IPaddress:port 對。
Port(端口)是一個 16 位的整數,用來標識不同的進程,利用不同的端口來連接不同的服務:對於客戶端,其端口是由內核隨機分配的,成爲臨時端口;對於服務器,一般使用知名端口,比如Web服務器通常使用80;電子郵件使用25。。。
對於服務器,客戶端我們只需要完成以上步驟就可以進行通信了。
我們通過編寫一個echo服務器來熟悉以上socket函數的使用,具體功能如下:
client發送信息到server,server接收信息回顯到client
效果圖如下:
代碼如下:
makefile
.PHONY:all
all:tcp_server tcp_client
tcp_server:tcp_server.c
gcc -o $@ $^ -static
tcp_client:tcp_client.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f tcp_client tcp_server
tcp_server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
void usage(const char* arg)
{
printf("correct usage : %s [ip] [port]\n", arg);
}
int start_up(const char* ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_aton(ip, &local.sin_addr);
//local.sin_addr.s_addr = inet_addr(ip);
// int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// 保證服務器能夠立即終止和重啓,默認重啓是等待30秒
int opt = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
fprintf(stderr, "setsockopt failure\n");
exit(-1);
}
int flag = bind(sock, (struct sockaddr*)&local, sizeof(local));
if (flag == -1)
{
fprintf(stderr, "bind failure\n");
exit(-1);
}
flag = listen(sock, 6);
if (flag == -1)
{
fprintf(stderr, "listen failure\n");
exit(-1);
}
return sock;
}
int main(int argc, char* argv[])
{
// ./tcp_server ip port
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int listen_sock = start_up(argv[1], atoi(argv[2]));
char buf[1024];
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int new_fd = accept(listen_sock, (struct sockaddr*)&client, &len);
if (new_fd == -1)
{
fprintf(stderr, "accept failure\n");
continue;
}
printf("a new client connects the server\n");
while (1)
{
memset(buf, 0, sizeof(buf));
ssize_t s = read(new_fd, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0;
printf("client [%s:%d] # %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buf);
write(new_fd, buf, strlen(buf));
}
else if (s == 0)
{
printf("client quits\n");
close(new_fd);
break;
}
else
{
printf("read failure\n");
close(new_fd);
break;
}
}
}
return 0;
}
tcp_client
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
void usage(const char* arg)
{
printf("correct usage: %s [remote_ip] [remote_port]\n", arg);
}
int open_clientfd(const char* ip, int port)
{
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in remote;
remote.sin_family = AF_INET;
remote.sin_port = htons(port);
inet_aton(ip, &remote.sin_addr);
int flag = connect(clientfd, (struct sockaddr*)&remote, sizeof(remote));
if (flag == -1)
{
fprintf(stderr, "connect failure\n");
exit(-1);
}
return clientfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock_fd = open_clientfd(argv[1], atoi(argv[2]));
while (1)
{
char buf[1024];
printf("please enter # ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s - 1] = '\0';
write(sock_fd, buf, strlen(buf));
//server echo
s = read(sock_fd, buf, strlen(buf));
if (s > 0)
{
printf("server echo # %s\n", buf);
}
}
}
return 0;
}
tcp_server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
void usage(const char* arg)
{
printf("correct usage: %s [remote_ip] [remote_port]\n", arg);
}
int open_clientfd(const char* ip, int port)
{
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in remote;
remote.sin_family = AF_INET;
remote.sin_port = htons(port);
inet_aton(ip, &remote.sin_addr);
int flag = connect(clientfd, (struct sockaddr*)&remote, sizeof(remote));
if (flag == -1)
{
fprintf(stderr, "connect failure\n");
exit(-1);
}
return clientfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock_fd = open_clientfd(argv[1], atoi(argv[2]));
while (1)
{
char buf[1024];
printf("please enter # ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s - 1] = '\0';
write(sock_fd, buf, strlen(buf));
//server echo
s = read(sock_fd, buf, strlen(buf));
if (s > 0)
{
printf("server echo # %s\n", buf);
}
}
}
return 0;
}