上一次,我們介紹了套接字的概念及簡單的UDP網絡程序 戳這裏查看;今天,我們介紹一個簡單的TCP網絡程序。
一. 地址轉換函數
在IPv4的socket網絡編程中,sockaddr_in中的成員dtruct in_addr sin_addr表示的是32位的IP地址,但是我們通常卻是用點分十進制的字符串表示。因此,我們在使用時,經常需要互相轉換。
1. 字符串轉in_addr的函數:
#include <arpa/inet.h>
int inet_aton(const char* strptr, struct in_addr* addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family, const char* strptr, void* addrptr);
2. in_addr轉字符串的函數:
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
其中,inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPV6的in6_addr,因此,函數接口是void* addptr。
3.關於inet_ntoa
inet_ntoa這個函數返回了一個char*,但是這個函數是在自己內部爲我們申請內存存放結果,那麼是否需要我們手動釋放?
man手冊上說,inet_ntoa函數,是把結果放到了靜態存儲區,不需要我們手動釋放。因此,這個函數,在使用的時候就需要注意,如果連續調用,第二次調用時的結果會覆蓋掉上一次的結果。
APUE中,明確提出inet_ntoa不是一個線程安全的函數;在多線程環境下,還是推薦使用inet_ntop,這個函數由調用者提供一個緩衝區保存結果,可以規避線程安全問題。
簡單的TCP網絡程序
服務器端
1. 常用API
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- socket()打開一個網絡通訊端口,就像open打開文件描述符一樣,調用時報返回-1,調用成功返回文件描述符;
- 應用程序可以向像對寫文件一樣,使用write/read在網絡上收發數據;
- 對於IPv4,第一個參數指定爲AF_INET;
- 對於TCP協議,第二個參數指定爲SOCK_STREAM,表示面向流的傳輸協議;對於UDP協議,第二個參數指定爲SOC_DGRAM;
- 對於第三個參數protocol,一般指定爲0即可。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 服務器所監聽的網絡地址和端口號通常是固定不變的,客服端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,服務器需要調用bind綁定一個固定的網絡地址和端口號;
- bind()調用成功返回0,失敗返回-1;
- struct sockaddr*是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結構體,而他們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。
- 對struct socket_in中的sin_addr.s_addr可以使用INADDR_ANY初始化,這個宏表示本地的任意地址,服務器可能有多個網卡,每個網卡也可能綁定多個IP,這樣設置可以再所有的IP地址上監聽,直到與某個客戶端建立連接才確定下來
addr.sin_addr.s_addr = htonl(INADDR_ANY);
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- listen()聲明sockfd處於監聽狀態,並且允許backlog個客戶端處於等待連接狀態,一般設置爲5;
- 參數backlog等待隊列,一般設置爲5,不會太大,太長會花費資源在維護隊列上;沒有的話,不能使服務器隨時處於滿載狀態;
- listen()成功返回0,失敗返回-1。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- TCP三次握手後,服務器調用accept()接受連接;
- 如果調用accpet時還沒有客戶端的連接請求,就阻塞等到有客戶端來連接;
- addr是一個傳出型參數,調用後傳出客戶端的地址和端口號,如果傳NULL,表示不關心客戶端的地址;
- accept的返回值纔是真正使用的套接字,最開始創建的是監聽套接字。舉一個例子,就像賣手機的店一樣,有的工作人員在外面向店裏拉客,而進來的客人是由另外的工作人員爲其介紹手機。
2. 服務器端代碼
服務器端的作用是接受client的請求,並將接受到的client的數據,返回給client。
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
int startup(char* ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
printf("create socket error, errno is:%d, errstring is:%s\n", errno, strerror(errno));
exit(1);
}
struct sockaddr_in server_socket;
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(port);
server_socket.sin_addr.s_addr = inet_addr(ip);
if(bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0)
{
printf("bind error, errno is:%d, errstring is:%s\n", errno, strerror(errno));
close(sock);
exit(2);
}
if(listen(sock, 5) < 0)
{
printf("listen error, error code is:%d, errstring is:%s\n", errno, strerror(errno));
close(sock);
exit(3);
}
printf("bind and listen success\n");
return sock;
}
void service(int sock, char* ip, int port)
{
while(1)
{
char buf[1024]={0};
ssize_t s = read(sock, buf, sizeof(buf));
if(s < 0)
{
perror("read");
exit(5);
}
else if(s > 0)
{
buf[s-1] = 0;
if(strcmp(buf,"quit")==0)
{
printf("client quit ....\n");
break;
}
printf("client[%s][%d]:%s\n", ip, port, buf);
write(sock, buf, sizeof(buf));
}
else //read finish
{
printf("client quit ....\n");
close(sock);
break;
}
}
printf("connect end.....Please wait.....\n");
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [ip][port]\n", argv[0]);
exit(4);
}
int listen_sock = startup(argv[1], atoi(argv[2]));//監聽套接字
struct sockaddr_in peer;//接受客戶端的地址和端口號
for(;;)
{
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);//接受連接
if(new_sock < 0)
{
printf("accept error, error code is:%d, errstring is:%s\n", errno, strerror(errno));
continue;
}
char buf[32];
memset(buf, 0, sizeof(buf));
inet_ntop(AF_INET, &peer.sin_addr, buf, sizeof(buf));
printf("connect success! ip is:%s, port is:%d\n", buf, ntohs(peer.sin_port));
service(new_sock, buf, ntohs(peer.sin_port));//服務器端執行的操作
close(new_sock);
}
close(listen_sock);
return 0;
}
客戶端
1. 常用API
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 客戶端不需要固定的端口號,因此不必調用bind,它的端口號由內核自動分配;
- 客戶端需要調用connect(),連接服務器;成功返回0,失敗返回-1;
2. 客戶端代碼
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<arpa/inet.h>
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage %s[ip]\n", argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in local;
local.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &local.sin_addr);
// local.sin_addr.s_addr = inet_addr(argv[1]);
local.sin_port = htons(8080);
int ret = connect(sock, (struct sockaddr*)&local, sizeof(local));
if(ret < 0)
{
printf("connect errno, error code is:%d, errstring is:%s\n", errno, strerror(errno));
exit(2);
}
printf("connect success! \n");
while(1)
{
char buf[1024]={0};
printf("client# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s-1] = 0;
write(sock, buf, sizeof(buf));
if(strcmp(buf, "quit") == 0)
{
printf("client quit\n");
break;
}
read(sock, buf, sizeof(buf));
printf("server# %s\n", buf);
}
}
close(sock);
return 0;
}
測試程序
1. 測試服務器
可以看到程序時處於監聽狀態的。
2.測試客戶端
3.多進程與多線程
我們上面的程序中,再啓動一個客戶端,嘗試連接服務器,發現不能連接;這是因爲,我們accpet了一個請求之後,一直在循環read,沒有辦法繼續調用accept,所以不能接受新的請求。
多進程版
1. 主函數如上圖所示,startup函數與service函數與上面的程序是一樣的;
2. 我們創建了一個子進程,在子進程中又創建了一個進程,我們讓孫子進程執行read操作,然後讓子進程立即退出,這樣父進程等待子進程的時間就會很短了;而孫子進程如果退出,會被系統回收。
3. 多進程版本的特點:
a. 易於編寫代碼,
b. 比較穩定(進程的獨立性,彼此之間不影響)
c. 服務器接accept之後,纔開始創建進程,佔時間;
d. 進程創建佔用資源較大,同時服務的人數有限;
e. 切換進程的成本較大,影響性能。
多線程版
1. work函數爲線程需要執行的程序,將sock,port和ip定義爲一個結構體,通過結構體傳參;
2. 多線程的特點
a. 創建成本小於進程,但也是有一定的成本;
b. 佔用資源小於進程,但也是同時服務的人數也是有限的;
c. 切換成本小於進程,但也是需要成本的;
d. 多線程不穩定,如果其中一個線程掛了,整個進程都會出現問題;
e. 編寫多線程程序,還需要注意線程安全的問題。