相關博文:html
接着該上篇博文,我們繼續,首先,爲了內容的完整性和連續性,咱們首要的是立馬補充、展現客戶端的示例代碼。linux
在此以後,以後我們有兩個方向:編程
一是介紹客戶端、服務器編程中一些注意事項,如鏈接斷開、獲取鏈接狀態等場景。服務器
一是基於以前的服務器端代碼只是基礎功能,在支持多客戶端訪問時將面臨困局,進一步,咱們須要介紹服務器併發編程模型。網絡
客戶端代碼併發
#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;
}
服務器代碼app
#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:代碼中的備註比較重要,請詳細參考。socket
服務器代碼內使用條件編譯,共有4個case. 思路以下。tcp
case 1, 基本服務器功能,客戶端發數據,服務器收數據代碼展現。 函數
case 2 、三、4 都是鏈接斷開時的一些狀況
case 2 展現了服務器主動關閉socket鏈接,對客戶端的影響。
case 2, 服務器在收到客戶端的一包數據後,就關閉該鏈接。若是客戶端繼續向此鏈接發數據,那麼將致使客戶端收到13號信號,即SIGPIPE,該信號的默認操做是使進程退出。
case 三、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函數,須要包含此頭文件。
.