相关博文:
系统编程-网络-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
", 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
", 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
");
exit(EXIT_FAILURE);
}
char sendbuf[1024];
char recvbuf[2014];
while (1)
{
fgets(sendbuf, sizeof(sendbuf), stdin);
printf("strlen(sendbuf) = %d
", strlen(sendbuf));
if (strcmp(sendbuf, "exit
") == 0){
printf("while(1) -> exit
");
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
");
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
", 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
", inet_ntop(AF_INET, &sin.sin_addr.s_addr, buf, sizeof(buf)));
printf("buf = %s
", 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.
");
char sned_buf[] = "hello, i am server
";
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
", 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 ....
");
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
", ip, port);
#endif
char client_buf[100]={0};
#if 1 // case 1:base function
while(1){
printf("server goes to read...
");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d
", bytes_read_done);
usleep(500000);
}
printf("server process end...
");
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...
");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d
", bytes_read_done);
close(new_client_fd);
while(1);
}
printf("server process end...
");
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
";
while(1){
printf("server goes to read...
");
int bytes_read_done = read(new_client_fd, client_buf, sizeof(client_buf));
printf("bytes_read_done = %d
", bytes_read_done);
if(bytes_read_done <= 0){
if(errno == EINTR){
/*** 对于EINTR的解释 见下方备注 */
printf("network may be ok
");
}
else
{
printf("network is not alive
");
}
}
int bytes = read(new_client_fd, client_buf, sizeof(client_buf));
printf("==> bytes = %d
", bytes);
if(bytes <= 0){
if(errno == EINTR){
printf("network may be ok ...
");
}
else
{
printf("network is not alive ...
");
}
}
// 实测,在客户端已经断开连接的情况下,该send函数仍然返回了 strlen(sendbuf)的有效长度。所以,我们不必寄希望于单纯通过send来获取客户端连接状态信息。
int bytes_send_done = send(new_client_fd, sendbuf, strlen(sendbuf), 0);
printf("bytes_send_done = %d
", bytes_send_done);
while(1){
printf("server is IDLE ...
");
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 !
");
}else{
printf("client is disconnected !
");
}
while(1){
printf("server is IDLE ...
");
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 判断客户端连接状态, 这种方法实时性高, 推荐使用。
补充: 也可以采用case 2 的类似方法来判断客户端是否断开连接,即:如果连接已断开,那么write的返回值将小于0,同时,errno会被设置为EPIPE。
相关知识点:
1. 对于EINTR的解释
一些IO系统调用执行时,如 read 等待输入期间,如果收到一个信号,系统将中断read, 转而执行信号处理函数.
当信号处理返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败?
早期UNIX系统的做法是, 中断系统调用,并让系统调用失败, 比如read返回 -1, 同时设置 errno 为EINTR.
中断了的系统调用是没有完成的调用,它的失败是临时性的,如果再次调用则可能成功,这并不是真正的失败.
所以要对这种情况进行处理。
2.
在Linux网络编程这块,胡乱包含过多头文件会导致编译不过。
//#include <linux/tcp.h> // 包含下方这个头文件,就不能包含该头文件,否则编译报错。
#include <netinet/tcp.h> // 使用getsockopt、setsockopt函数,需要包含此头文件。
.