相關博文:
系統程式設計-網路-tcp客戶端伺服器程式設計模型、socket、htons、inet_ntop等各API詳解、使用telnet測試基本伺服器功能
接著該上篇博文,我們們繼續,首先,為了內容的完整性和連續性,我們首要的是立馬補充、展示客戶端的示例程式碼。
在此之後,之後我們們有兩個方向:
一是介紹客戶端、伺服器程式設計中一些注意事項,如連線斷開、獲取連線狀態等場景。
一是基於之前的伺服器端程式碼只是基礎功能,在支援多客戶端訪問時將面臨困局,進一步,我們需要介紹伺服器併發程式設計模型。
客戶端程式碼
#include <unistd.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<string.h>
#include<errno.h>
#include<stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define PORT 5001
#define SERVER_IP "192.168.1.21"
void sig_handler(int signo){
printf("sig_handler=> pid: %d, signo: %d \n", getpid(), signo);
}
// 如果使用ctrl+c 終止該程式,伺服器也會收到斷開連線事件,
// 可見是作業系統底層幫應用程式擦屁股了。
// 直接呼叫close來關閉該連線,會使得伺服器收到斷開連線事件。
int main()
{
int sockfd;
struct sockaddr_in server_addr;
struct hostent *host;
if(signal(SIGPIPE, sig_handler) == SIG_ERR){
//if(signal(SIGPIPE, SIG_DFL) == SIG_ERR){ // SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以本程式會退出。
perror("signal error");
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Socket Error is %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
{
fprintf(stderr, "Connect failed\n");
exit(EXIT_FAILURE);
}
char sendbuf[1024];
char recvbuf[2014];
while (1)
{
fgets(sendbuf, sizeof(sendbuf), stdin);
printf("strlen(sendbuf) = %d \n", strlen(sendbuf));
if (strcmp(sendbuf, "exit\n") == 0){
printf("while(1) -> exit \n");
break;
}
send(sockfd, sendbuf, strlen(sendbuf), 0);
//recv(sockfd, recvbuf, sizeof(recvbuf), 0);
//fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
//memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
printf(" client process end \n");
return 0;
}
伺服器程式碼
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include "server.h"
#include <assert.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
// 在Linux網路程式設計這塊,,胡亂包含過多標頭檔案會導致編譯不過。
//#include <linux/tcp.h> // 包含下方這個標頭檔案,就不能包含該標頭檔案,否則編譯報錯。
#include <netinet/tcp.h> // setsockopt函式需要包含此標頭檔案
int server_local_fd, new_client_fd;
void sig_deal(int signum){
close(new_client_fd);
close(server_local_fd);
exit(1);
}
int main(void)
{
struct sockaddr_in sin;
signal(SIGINT, sig_deal);
printf("pid = %d \n", getpid());
/*1.建立IPV4的TCP套接字 */
server_local_fd = socket(AF_INET, SOCK_STREAM, 0);
if(server_local_fd < 0) {
perror("socket error!");
exit(1);
}
/* 2.繫結在伺服器的IP地址和埠號上*/
/* 2.1 填充struct sockaddr_in結構體*/
bzero(&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
#if 0
// 方式一
sin.sin_addr.s_addr = inet_addr(SERV_IPADDR);
#endif
#if 0
// 方式二:
sin.sin_addr.s_addr = INADDR_ANY;
#endif
#if 1
// 方式三: inet_pton函式來填充此sin.sin_addr.s_addr成員
if(inet_pton(AF_INET, "192.168.1.21", &sin.sin_addr.s_addr) >0 ){
char buf[16] = {0};
printf("s_addr=%s \n", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
printf("buf = %s \n", buf);
}
#endif
/* 2.2 繫結*/
if(bind(server_local_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
perror("bind");
exit(1);
}
/*3.listen */
listen(server_local_fd, 5);
printf("client listen 5. \n");
char sned_buf[] = "hello, i am server \n";
struct sockaddr_in clientaddr;
socklen_t clientaddrlen;
/*4. accept阻塞等待客戶端連線請求 */
#if 0
/*****不關心連線上來的客戶端的資訊*****/
if( (new_client_fd = accept(server_local_fd, NULL, NULL)) < 0) {
}else{
/*5.和客戶端進行資訊的互動(讀、寫) */
ssize_t write_done = write(new_client_fd, sned_buf, sizeof(sned_buf));
printf("write %ld bytes done \n", write_done);
}
#else
/****獲取連線上來的客戶端的資訊******/
memset(&clientaddr, 0, sizeof(clientaddr));
memset(&clientaddrlen, 0, sizeof(clientaddrlen));
clientaddrlen = sizeof(clientaddr);
/***
* 由於cliaddr_len是一個傳入傳出引數(value-result argument),
* 傳入的是呼叫者提供的緩衝區的長度以避免緩衝區溢位問題,
* 傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿呼叫者提供的緩衝區).
* 所以,每次呼叫accept()之前應該重新賦初值。
* ******/
if( (new_client_fd = accept(server_local_fd, (struct sockaddr*)&clientaddr, &clientaddrlen)) < 0) {
perror("accept");
exit(1);
}
printf("client connected! print the client info .... \n");
int port = ntohs(clientaddr.sin_port);
char ip[16] = {0};
inet_ntop(AF_INET, &(clientaddr.sin_addr.s_addr), ip, sizeof(ip));
printf("client: ip=%s, port=%d \n", ip, port);
#endif
char client_buf[100]={0};
#if 1 // case 1:base function
while(1){
printf("server goes to read... \n");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d \n", bytes_read_done);
usleep(500000);
}
printf("server process end... \n");
close(new_client_fd);
close(server_local_fd);
#endif
#if 0 // case 2 : 當伺服器close一個連線時,若client端接著發資料。系統會發出一個SIGPIPE訊號給客戶端程式,告知這個連線已經斷開了,不要再寫了。
// SIGPIPE訊號的預設執行動作是terminate(終止、退出),所以client會退出。若不想客戶端退出可以把SIGPIPE設為SIG_IGN
// 在linux下寫socket的程式的時候,如果嘗試send到一個disconnected socket上,就會讓底層丟擲一個SIGPIPE訊號。
// 驗證方法,伺服器這裡收到一次客戶端訊息後,就關閉該客戶端的描述符。然後客戶端內繼續向此socket傳送資料,觀察客戶端內程式碼的執行效果。
while(1){
printf("server goes to read... \n");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d \n", bytes_read_done);
close(new_client_fd);
while(1);
}
printf("server process end... \n");
close(server_local_fd);
#endif
#if 0 //case 3 : read()返回值小於等於0時,socket連線有可能斷開。此時,需要進一步判斷errno是否等於EINTR,
// 如果errno == EINTR,則說明recv函式是由於程式接收到訊號後返回的,socket連線還是正常的,不應close掉該socket連線。
// 如果errno != EINTR,則說明客戶端已斷開連線,則伺服器端可以close掉該socket連線。
if(signal(SIGPIPE, SIG_DFL) == SIG_ERR){
perror("signal error");
}
char sendbuf[1024] = "hello i am server\n";
while(1){
printf("server goes to read... \n");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d \n", bytes_read_done);
if(bytes_read_done <= 0){
if(errno == EINTR){
/*** 對於EINTR的解釋 見下方備註 */
printf("network may be ok \n");
}
else
{
printf("network is not alive \n");
}
}
int bytes = read(new_client_fd, client_buf, sizeof(client_buf));
printf("==> bytes = %d \n", bytes);
if(bytes <= 0){
if(errno == EINTR){
printf("network may be ok ...\n");
}
else
{
printf("network is not alive ...\n");
}
}
// 實測,在客戶端已經斷開連線的情況下,該send函式仍然返回了 strlen(sendbuf)的有效長度。所以,我們不必寄希望於單純通過send來獲取客戶端連線狀態資訊。
int bytes_send_done = send(new_client_fd, sendbuf, strlen(sendbuf), 0);
printf("bytes_send_done = %d \n", bytes_send_done);
while(1){
printf("server is IDLE ... \n");
usleep(500000);
}
}
close(new_client_fd);
close(server_local_fd);
/*** 對於EINTR的解釋
* 一些IO系統呼叫執行時,如 read 等待輸入期間,如果收到一個訊號,系統將中斷read, 轉而執行訊號處理函式.
* 當訊號處理返回後, 系統遇到了一個問題: 是重新開始這個系統呼叫, 還是讓系統呼叫失敗?
* 早期UNIX系統的做法是, 中斷系統呼叫,並讓系統呼叫失敗, 比如read返回 -1, 同時設定 errno 為EINTR.
* 中斷了的系統呼叫是沒有完成的呼叫,它的失敗是臨時性的,如果再次呼叫則可能成功,這並不是真正的失敗.
* 所以要對這種情況進行處理,
***/
#endif
#if 0 //case 4: 使用 getsockopt 實時判斷客戶端連線狀態 實時性高
while(1){
sleep(10); // 你可以在這10秒內進行操作,讓客戶端程式退出,或者讓其保持正常連線
struct tcp_info info;
int len = sizeof(info);
getsockopt(new_client_fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);
if((info.tcpi_state == TCP_ESTABLISHED)){
printf("client is connected !\n");
}else{
printf("client is disconnected !\n");
}
while(1){
printf("server is IDLE ... \n");
usleep(500000);
}
}
close(new_client_fd);
close(server_local_fd);
#endif
return 0;
}
PS:程式碼中的備註比較重要,請詳細參考。
伺服器程式碼內使用條件編譯,共有4個case. 思路如下。
case 1, 基本伺服器功能,客戶端發資料,伺服器收資料程式碼展示。
case 2 、3、4 都是連線斷開時的一些情況
case 2 展示了伺服器主動關閉socket連線,對客戶端的影響。
case 2, 伺服器在收到客戶端的一包資料後,就關閉該連線。如果客戶端繼續向此連線發資料,那麼將導致客戶端收到13號訊號,即SIGPIPE,該訊號的預設操作是使程式退出。
case 3、4 展示了客戶端斷開連線(在客戶端中斷內敲入exit,即可使得客戶端程式退出)後,伺服器端如何判斷該連線是否已斷開的方法。
case 3, read()返回值小於等於0時,socket連線有可能斷開。此時,需要進一步判斷errno是否等於EINTR。
如果errno == EINTR,則說明recv函式是由於程式接收到訊號後返回的,socket連線還是正常的,不應close掉該socket連線。
如果errno != EINTR,則說明客戶端已斷開連線,則伺服器端可以close掉該socket連線。
case 4,使用 getsockopt 判斷客戶端連線狀態, 這種方法實時性高, 推薦使用。
相關知識點:
1. 對於EINTR的解釋
一些IO系統呼叫執行時,如 read 等待輸入期間,如果收到一個訊號,系統將中斷read, 轉而執行訊號處理函式.
當訊號處理返回後, 系統遇到了一個問題: 是重新開始這個系統呼叫, 還是讓系統呼叫失敗?
早期UNIX系統的做法是, 中斷系統呼叫,並讓系統呼叫失敗, 比如read返回 -1, 同時設定 errno 為EINTR.
中斷了的系統呼叫是沒有完成的呼叫,它的失敗是臨時性的,如果再次呼叫則可能成功,這並不是真正的失敗.
所以要對這種情況進行處理。
2.
在Linux網路程式設計這塊,胡亂包含過多標頭檔案會導致編譯不過。
//#include <linux/tcp.h> // 包含下方這個標頭檔案,就不能包含該標頭檔案,否則編譯報錯。
#include <netinet/tcp.h> // 使用getsockopt、setsockopt函式,需要包含此標頭檔案。
.