通过学习Unix网络编程卷一:套接字联网API,实现了一个完整的TCP客户/服务器程序示例,这个例子执行如下步骤构建了一个基本的回射服务器:
1. 客户从标准输入读入数据,并发送给服务器;
2. 服务器从网络输入读入数据,进行处理后回射给客户;
3. 客户从网络输入读入数据,并在标准输出显示。
首先是服务器程序:
#include <sys/socket.h>
#include <sys/types.h> // 提供pid_t size_t ssize_t等类型的定义
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <sys/wait.h> // 提供wait()函数的定义
#include <signal.h> // 提供signal()函数的定义
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
#include "sum.h"
#define SERV_PORT 9877 // 服务器端口
#define LISTENQ 1024 // listen()的第二个参数backlog值
#define MAXLINE 4096 // 最大文本行数
#define BUFFSIZE 8192 // 读写缓冲区大小
using namespace std;
// 处理每个客户的服务:从客户读入数据,并把它们回射给客户
void str_echo(int sockfd)
{
// size_t和ssize_t都是用来提高程序的可移植性的
// ssize_t是有符号整型,等同于int(32位机器)/long(64位机器)
// size_t就是无符号型的ssize_t,也就是unsigned int(32位)/unsigned long(64位)
// 用法区别:size_t一般用于缓冲区大小这种非负的场景,而对于像read/write等函数,可能
// 失败返回负数的时候用ssize_t
ssize_t m, n;
char buf[MAXLINE]; //read缓冲区
again:
// read函数从打开的设备或文件中读取数据
// ssize_t read(int sockfd, void* buff, size_t n) 从sockfd中读n字节数据到buff中
// 读取成功返回读取的字节数,失败返回-1并设置errno,如果调用read之前已达文件末尾,返回0
// write函数向打开的设备或文件中写数据
// ssize_t write(int sockfd, void* buff, size_t n) 向sockfd中写n字节数据到buff中
// 写成功返回写入的字节数,失败返回-1并设置errno
// 两个函数的头文件为 #include <unistd.h>
// socket编程接口提供了几个专门用于socket数据读写的系统调用,用于TCP流数据读写的是:
// #include <sys/types.h>
// #include <sys/socket.h>
// ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 参数和返回值情况跟read和write函数一样,flags通常设置为0。
while((m = recv(sockfd, buf, MAXLINE, 0)) > 0)
{
cout <<"recv data: "<< buf << endl;
// strlen函数在计数时遇到'\0'才停止,而接收到的数据没有'\0',需要在末尾加上'\0'
buf[m] = '\0';
n = send(sockfd, buf, strlen(buf), 0);
}
if(m < 0 && errno == EINTR)
goto again;
else if(m < 0)
cout << "str_echo: read error" << endl;
else if(errno == EINTR)
cout << "recv/send error" << endl;
}
// 对两个数求和的处理函数(字符串转换)
void str_echo_sum(int sockfd)
{
long arg1, arg2;
ssize_t n;
char buf[MAXLINE];
for( ; ; )
{
if((n = recv(sockfd, buf, MAXLINE, 0)) == 0)
return;
buf[n] = '\0';
if(sscanf(buf, "%ld%ld", &arg1, &arg2) == 2)
{
snprintf(buf, sizeof(buf), "%ld", arg1 + arg2);
}
else
snprintf(buf, sizeof(buf), "input error\n");
send(sockfd, buf, strlen(buf), 0);
}
}
// 对两个数求和的处理函数(二进制字节流)
void str_echo_byte(int sockfd)
{
ssize_t n;
struct args args;
struct result result;
for( ; ; )
{
if((n = recv(sockfd, &args, sizeof(args), 0)) == 0)
return;
result.sum = args.arg1 + args.arg2;
send(sockfd, &result, sizeof(result), 0);
cout << args.arg1 << "***" << args.arg2;
}
}
// SIGCHLD信号的处理函数,触发时用来处理僵尸进程
void sig_chld(int signo)
{
pid_t pid;
int stat;
// wait 和 waitpid函数原型
// #include <sys/wait.h>
// pid_t wait(int *statloc);
// pid_t waitpid(pid_t pid, int *statloc, int options);
// 返回值:成功均返回(已终止子进程的)进程ID,出错返回0或-1(通过statloc指针返回子进程终止状态(int))
// pid = wait(&stat);
// 用waitpid不用wait因为可以通过WNOHANG选项告知waitpid在尚有未终止的子进程在运行时不要阻塞
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
cout << "child " << pid << "terminated" << endl;
// 警告:在信号处理函数中调用I/O函数是不合适的,此处是为了查看子进程的状态
return;
}
//dup函数测试
void str_dup(int fd)
{
close(STDOUT_FILENO);
// dup/dup2函数用于复制文件描述符,可以实现把标准输入重定向到一个文件,
// 或者把标准输出重定向到一个网络连接(比如CGI编程)。
// #include <unistd.h>
// int dup(int file_descriptor);
// int dup2(int file_descriptor_one, int file_descriptor_two);
// 函数成功返回系统当前可用的最小整数值,失败返回-1并设置errno。
dup(fd);
char words[MAXLINE];
cout << "hello world!" <<endl;
}
int main(int argc, char* argv[])
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
// 创建socket:int socket(int domain, int type, int protocol);
// domain参数标识底层协议族,参数值为PF_INET(用于ipv4)或PF_INET6(用于IPv6)。
// type参数指定服务类型,服务类型(参数值)为SOCK_STREAM服务(流服务,表示使用TCP协议)
// 和SOCK_UGRAM(数据报,表示使用UDP协议)服务。
// protocol参数在前两个参数确定后再选择的一个具体的协议,几乎所有的情况都设置为0,表示使用默认协议。
listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert( listenfd >= 0 );
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// INADDR_ANY为通配地址(inet_addr("0.0.0.0"))
// 作用是当服务器有多个网卡(对应多个IP地址)的时候,接收所有发到服务器的数据,与IP无关。
servaddr.sin_port = htons(SERV_PORT);
// bind函数把一个本地协议地址赋予一个套接字
// #include <sys/socket.h>
// int bind(int sockfd, const struct sockaddr* address, sizeof(address));
// 返回值:成功返回0,出错返回-1,
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
// listen函数用来监听套接字,仅为TCP调用。
// #include <sys/socket.h>
// int listen(int sockfd, int backlog);
// 返回值:成功返回0,出错返回-1。
// backlog包括处于SYN_RCVD状态的未完成连接和处于ESTABLISTENED状态的已完成连接,一般设为5。
listen(listenfd, LISTENQ);
// 俘获SIGCHLD信号,用来处理僵尸进程
// 在listen调用之后调用此函数,并且要在fork第一个子进程之前完成,且只做一次!
signal(SIGCHLD, sig_chld);
//signal(SIGCHLD, SIG_IGN);
// 一般把SIGCHLD信号的处理设定为SIG_IGN也是可行的,sig_child函数增加了可移植性
for( ; ; )
{
// 服务器阻塞于accept调用,等待客户端连接的完成。
clilen = sizeof(cliaddr);
// 下面if语句的作用是重启被中断的系统调用,同样适用于read/write/select/open等调用
if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen))< 0)
{
if(errno == EINTR)
continue;
else
cout << "accept error" << endl;
}
// fork函数(包括有些系统提供的其变体)是Unix派生新进程的唯一方法。
// #include <unistd.h>
// pid_t fork(void);
// 返回值:在父进程中返回子进程ID,子进程中返回0(子进程的父进程唯一,可以通过getppid取得父进程ID),出错返回-1。
// fork为每一个客户派生一个处理他们的子进程。
// fork产生子进程时,从父进程那里复制listen调用和accept调用。
// 子进程关闭listen调用,处理跟客户的连接
// 父进程关闭accept调用,可以在listen套接字上再次调用accept处理下一个客户连接。
if((childpid = fork()) == 0)
{
close(listenfd);
str_echo(connfd); // 基本的回射服务器,同时可用来测试select系统调用
// str_echo_sum(connfd); // 从客户端接收字符串,返回long型和
//str_echo_byte(connfd); // 从客户端接收字节流,返回连个数据和
// str_dup(connfd); // 测试dup函数
exit(0);
}
close(connfd);
}
}