本文主要基于《TCP/IP 网络编程》这本书进行总结,主要针对 Linux 网络编程部分进行阐述,Windows 网络编程部分有需要建议阅读原书籍。

一、基础知识

网络编程

网络编程就是编写程序使两台连网的计算机相互交换数据。

套接字

套接字是网络数据传输用的软件设备。我们把插头插到插座上就能从电网获得电力供给,同样为了与远程计算机进行数据传输, 需要连接到因特网,而编程中的"套接字"就是用来连接该网络的工具。它本身就带有"连接"的含义,如果将其一引申, 则还可以表示两台计算机之间的网络连接。

#include <sys/socket.h>int socket(int domain, int type, int protocol);
// 成功时返回文件描述符,失败时返回-1
/* domain 套接字中使用的协议族( Protocol Famjly )信息。type 套接字数据传输类型信息。protocol 计算机间通信中使用的协议信息。
*/

协议族 domain:

  • PF_INET IPv4互联网协议族

  • PF_INET6 IPv6互联网协议族

  • PF_LOCAL 本地通信的UNIX协议族

  • PF_PACKET 底层套接字的协议族

  • PF_IPX IPX Novel1协议族

套接字类型 type

  • 面向连接的套接字 SOCK_STREAM(TCP)

  • 面向消息的套接字 SOCK_DGRAM (UDP)

协议的最终选择 protocol

传递前两个参数即可创建所需套接字,大部分情况下可以向第三个参数传递0,除非同一协议族中存在多个数据传输方式相同的协议

// IPv4协议族中面向连接的套接字
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);// IPv4协议族中面向消息的套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

基于 Linux 的文件操作

对于 Linux 而言,socket 操作和文件操作没有区别,文件描述符就是系统分配给文件或套接字的整数。

打开文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *path , int flag);
// 成功时返回文件描述符,失败时返回-1
// path 文件名的字符串地址。 flag 文件打开模式信息。

关闭文件

#include <unistd.h>int close(int fd);
// 成功时返回0,失败时返回-1
// fd 需要关闭的文件或套接字的文件描述符

写入文件

#include <unistd.h>ssize_t write(int fd, const void* buf, size_t nbytes);
// 成功时返回写入的字节数,失败时返回-1
// fd 显示数据传输对象的文件描述符   buf 保存要传输数据的缓冲地址值   nbytes 要传输数据的字节数

读取文件

#include <unistd.h>ssize_t read(int fd, void * buf, size_t nbytes);
// 成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
// fd 显示数据接收对象的文件描述符   buf 保存要接收数据的缓冲地址值   nbytes 要接收数据的最大字节数

地址信息的表示与分配

通过结构体 sockaddr_in 将地址信息传入 bind 函数中

struct sockaddr_in
{sa_family_t    sin_family;     //地址族( Address Family )uint16_t       sin_port;       //16位 TCP/UDP 端口号struct in_addr sin_addr;       //32位 IP 地址char           sin_zero[8];    //不使用
}struct in_addr
{In_add_t       s_addr;         //32位 IPv4 地址
}

套接字创建过程中常见的网络地址信息初始化方法。

struct sockaddr_in addr;
char* serv_ip = "211.117.168.13";              //声明 IP 地址字符串
char* serv_port = "9190";                      //声明端口号字符串
memset(&addr, 0, sizeof(addr));                // 结构体变量 addr 的所有成员初始化为0
addr.sin_family = AF_INET;                     //指定地址族
// inet_addr 将字符串信息转换为网络字节序的整数型
addr.sin_addr.s_addr = inet_addr(serv_ip);     //基于字符串的 IP 地址初始化
// addr.sin_addr.s_addr = htonl(INADDR_ANY); 若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。
// htons 将主机字节序转换为网络字节序(大端序) s表示short型   l表示long型
addr.sin_port = htons(atoi(serv_port));        //基于字符串的端口号初始化

把初始化的地址信息分配给套接字。bind 函数负责这项操作。

#include <sys/socket.h>int bind(int sockfd , struct sockaddr* myaddr, socklen_t addrlen);
// 成功时返回0 ,失败时返回-1
/* sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符。myaddr 存有地址信息的结构体变量地址值。addrlen 第二个结构体变量的长度。
*/

进入等待连接请求状态

调用 listen 函数转为可接收请求状态。只有调用了listen函数,客户端才能进入可发出连接请求的状态。

#include <sys/socket.h>int listen(int sockfd, int backlog);
// 成功时返回0 ,失败时返回-1
/* sock     希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字) 。backlog  连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。
*/

受理客户端连接请求

调用 accept 函数受理连接请求。accept 函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时, accept 函数内部将产生用于数据 I/O 的套接字, 并返回其文件描述符。

#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 成功时返回文件描述符,失败时返回-1
/*sock      服务器套接字的文件描述符。addr      保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。addrlen   第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
*/

Hello World 服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>void error_handling(char *message);int main(int argc, char *argv[])
{int serv_sock;int clnt_sock;struct sockaddr_in serv_addr;struct sockaddr_in clnt_addr;socklen_t clnt_addr_size;char message[]="Hello World!";if(argc!=2){printf("Usage : %s <port>\n", argv[0]);exit(1);}// 创建套接字serv_sock=socket(PF_INET, SOCK_STREAM, 0);if(serv_sock == -1)error_handling("socket() error");// 初始化地址信息memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);serv_addr.sin_port=htons(atoi(argv[1]));// 将地址分配给套接字if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )error_handling("bind() error"); // 进入等待连接请求状态if(listen(serv_sock, 5)==-1)error_handling("listen() error");// 受理客户端连接请求clnt_addr_size=sizeof(clnt_addr);  clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);if(clnt_sock==-1)error_handling("accept() error");  // 通过 write 函数向客户端传输数据write(clnt_sock, message, sizeof(message));// 调用 close 函数关闭连接close(clnt_sock);    close(serv_sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

Hello World 客户端

客户端通过 connect 函数请求连接

#include <sys/socket.h>int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);
// 成功时返回0 ,失败时返回-1
/*sock        客户端套接字文件描述符。servaddr    保存目标服务器端地址信息的变量地址值addrlen     以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
*/

完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>void error_handling(char *message);int main(int argc, char* argv[])
{int sock;struct sockaddr_in serv_addr;char message[30];int str_len;if(argc!=3){printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}// 创建套接字sock=socket(PF_INET, SOCK_STREAM, 0);if(sock == -1)error_handling("socket() error");// 初始化地址信息 初始化值为目标服务器端套接字的IP和端口信息。memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=inet_addr(argv[1]);serv_addr.sin_port=htons(atoi(argv[2]));// 请求连接if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) error_handling("connect() error!");// 接收服务端传输的数据str_len=read(sock, message, sizeof(message)-1);if(str_len==-1)error_handling("read() error!");printf("Message from server: %s \n", message);  // 关闭连接close(sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

图片为函数调用关系

二、基于TCP的回声服务

回声( echo ) 服务器端/客户端就是将客户端传输的字符串数据原封不动地传回客户端。

基本运行方式:

  • 服务器端在同一时刻只与一个客户端相连,并提供回声服务。

  • 服务器端依次向5个客户端提供服务并退出。

  • 客户端接收用户输入的字符串并发送到服务器端。

  • 服务器端将接收的字符串数据传回客户端,即"回声" 。

  • 服务器端与客户端之间的字符串回声一直执行到客户端输入Q为止。

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 1024
void error_handling(char *message);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;char message[BUF_SIZE];int str_len, i;struct sockaddr_in serv_adr;struct sockaddr_in clnt_adr;socklen_t clnt_adr_sz;if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);   if(serv_sock==-1)error_handling("socket() error");memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");clnt_adr_sz=sizeof(clnt_adr);for(i=0; i<5; i++){clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);if(clnt_sock==-1)error_handling("accept() error");elseprintf("Connected client %d \n", i+1);while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)write(clnt_sock, message, str_len);close(clnt_sock);}close(serv_sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

客户端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 1024
void error_handling(char *message);int main(int argc, char *argv[])
{int sock;char message[BUF_SIZE];int str_len;struct sockaddr_in serv_adr;if(argc!=3) {printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}sock=socket(PF_INET, SOCK_STREAM, 0);   if(sock==-1)error_handling("socket() error");memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=inet_addr(argv[1]);serv_adr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("connect() error!");elseputs("Connected...........");while(1) {fputs("Input message(Q to quit): ", stdout);fgets(message, BUF_SIZE, stdin);if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))break;// 这里存在问题,TCP不存在数据边界,多次调用 write 函数传递的字符串有可能一次性传递到服务器端此时客户端有可能从服务     器端收到多个字符串服务器端希望通过调用1 次write函数传输数据,但如果数据太大,操作系统就有可能把数据分成多个数据包发          送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用read函数。//write(sock, message, strlen(message));//str_len=read(sock, message, BUF_SIZE-1);// 以上代码改为下面的recv_len=0;while(recv_len<str_len){recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);if(recv_cnt==-1)error_handling("read() error!");recv_len+=recv_cnt;}message[str_len]=0;printf("Message from server: %s", message);}close(sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

三、基于UDP的回声服务

UDP 提供的是不可靠的数据传输服务,但是在传输速度上要优于 TCP。更多 UDP 和 TCP 的细节请参考《计算机网络-自顶向下》。

UDP 中的服务器端和客户端没有连接,不必调用TCP连接过程中调用的 listen 函数和 accept 函数,只有创建套接字的过程和数据交换过程。

基于 UDP 的数据 I/O 函数

#include <sys/socket.h>ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
// 成功时返回传输的字节数, 失败时返回-1。
/*sock      用于传输数据的UDP套接字文件描述符。buff     保存待传输数据的缓冲地址值。nbytes        待传输的数据长度,以字节为单位。flags        可选项参数,若没有则传递0。to         存有目标地址信息的sockaddr结构体变量的地址值。addrlen      传递给参数to的地址值结构体变量长度。
*/
#include <sys/socket.h>ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
// 成功时返回接收的字节数, 失败时返回-1。
/*sock      用于接收数据的UDP套接字文件描述符。buff     保存接收数据的缓冲地址值。nbytes     可接收的最大字节数,故无法超过参数bu忏所指的缓冲大小。flags        可选项参数,若没有则传递0。from       存有发送端地址信息的sockaddr结构体变量的地址值。addrlen     保存参数from的结构体变量长度的变量地址值。
*/

基于 UDP 的回声客户端

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 30
void error_handling(char *message);int main(int argc, char *argv[])
{int serv_sock;char message[BUF_SIZE];int str_len;socklen_t clnt_adr_sz;struct sockaddr_in serv_adr, clnt_adr;if(argc!=2){printf("Usage : %s <port>\n", argv[0]);exit(1);}// 创建UDP套接字serv_sock=socket(PF_INET, SOCK_DGRAM, 0);if(serv_sock==-1)error_handling("UDP socket creation error");// 初始化地址信息memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));// 将地址分配给套接字if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");while(1) {clnt_adr_sz=sizeof(clnt_adr);// 利用分配的地址接收数据str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);// 上一行函数调用同时获取数据传输端的地址,利用该地址将数据回传sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);}  close(serv_sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

客户端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 30
void error_handling(char *message);int main(int argc, char *argv[])
{int sock;char message[BUF_SIZE];int str_len;socklen_t adr_sz;struct sockaddr_in serv_adr, from_adr;if(argc!=3){printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}// 创建UDP套接字sock=socket(PF_INET, SOCK_DGRAM, 0);   if(sock==-1)error_handling("socket() error");// 初始化地址信息memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=inet_addr(argv[1]);serv_adr.sin_port=htons(atoi(argv[2]));while(1){fputs("Insert message(q to quit): ", stdout);fgets(message, sizeof(message), stdin);     if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) break;// 向服务端传输数据sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));// jadr_sz=sizeof(from_adr);str_len=recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);message[str_len]=0;printf("Message from server: %s", message);}   close(sock);return 0;
}void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

四、多进程服务器端

进程

进程定义为“占用内存空间的正在运行的程序”

通过调用 fork 函数创建进程,fork 函数将创建调用的进程副本

#include <unistd.h>pid_t fork(void);
// 成功时返回进程ID,失败时返回-1

fork 函数使用示例:

#include <stdio.h>
#include <unistd.h>
int gval=10;int main(int argc, char *argv[])
{pid_t pid;int lval=20;gval++, lval+=5;pid=fork();        if(pid==0)    // if Child Processgval+=2, lval+=2;else            // if Parent Processgval-=2, lval-=2;if(pid==0)printf("Child Proc: [%d, %d] \n", gval, lval);elseprintf("Parent Proc: [%d, %d] \n", gval, lval);return 0;
}

僵尸进程

进程完成工作后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作"僵尸进程"。

如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存, 并让子进程长时间处于僵尸进程状态。

为了销毁子进程,父进程应主动请求获取子进程的返回值。销毁僵尸进程方法:

  • 利用 wait 函数:调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值

  • wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。

#include <sys/wait.h>pid_t waitpid(pid_t pid, int * statloc, int options);
// 成功时返回终止的子进程ID(或0),失败时返回-1
/*pid           等待终止的目标子进程的10 ,若传递- 1 ,则与wait函数相同,可以等待任意子进程终止。statloc      与wait函数的statloc参数具有相同含义。options     传递头文件中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。
*/

信号处理

子进程究竟何时终止?调用 waitpid 函数后要无休止地等待吗?如果操作系统可以在子进程终止时向父进程传递信号,让父进程转而处理子进程终止的相关事宜,此处的"信号"是在特定事件发生时由操作系统向进程发送的消息。

信号注册函数

#include <signal.h>void (*signal(int signo, void (*func)(int)))(int);
// 为了在产生信号时调用,返回之前注册的函数指针

信号处理示例

#include <stdio.h>
#include <unistd.h>
#include <signal.h>void timeout(int sig)
{// 已到通过调用alarm函数注册的时间。if(sig==SIGALRM)puts("Time out!");alarm(2);
}
void keycontrol(int sig)
{// 输入CTRL+Cif(sig==SIGINT)puts("CTRL+C pressed");
}int main(int argc, char *argv[])
{int i;signal(SIGALRM, timeout);signal(SIGINT, keycontrol);alarm(2);for(i=0; i<3; i++){puts("wait...");sleep(100);}return 0;
}

相比于 signal 函数,sigaction 函数更稳定,使用更多

#include <signal.h>int sigaction(int signo, const struct sigaction * act, struct sigaction *oldact);
// 成功时返回0,失败时返回-1
/*signo     与signal函数相同,传递信号信息。act           对应于第一个参数的信号处理函数(信号处理器) 信息。oldact        通过此参数获取之前注册的信号处理函数指针,若不需要则传递0。
*/
// 声明并初始化sigaction结构体变量以调用上述函数
struct sigaction
{// 信号处理函数的指针值void (*sa_handler)(int);// sa-mask和sa_flags的所有位均初始化为0即可,这2个成员用于指定信号相关的选项和特性sigset_t sa_mask;int sa_flags;
}

信号处理示例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>void timeout(int sig)
{if(sig==SIGALRM)puts("Time out!");alarm(2);
}int main(int argc, char *argv[])
{int i;struct sigaction act;act.sa_handler=timeout;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGALRM, &act, 0);alarm(2);for(i=0; i<3; i++){puts("wait...");sleep(100);}return 0;
}

利用信号处理消灭僵尸进程

子进程终止时将产生 SIGCHLD 信号,具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>void read_childproc(int sig)
{int status;pid_t id=waitpid(-1, &status, WNOHANG);if(WIFEXITED(status)){printf("Removed proc id: %d \n", id);printf("Child send: %d \n", WEXITSTATUS(status));}
}int main(int argc, char *argv[])
{pid_t pid;struct sigaction act;act.sa_handler=read_childproc;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGCHLD, &act, 0);pid=fork();if(pid==0){puts("Hi! I'm child process");sleep(10);return 12;}else{printf("Child proc id: %d \n", pid);pid=fork();if(pid==0){puts("Hi! I'm child process");sleep(10);exit(24);}else{int i;printf("Child proc id: %d \n", pid);for(i=0; i<5; i++){puts("wait...");sleep(5);}}}return 0;
}
/*
root@my_linux:/home/swyoon/tcpip# gcc remove_zombie.c -o zombie
root@my_linux:/home/swyoon/tcpip# ./zombie
Hi! I'm child process
Child proc id: 9529
Hi! I'm child process
Child proc id: 9530
wait...
wait...
Removed proc id: 9530
Child send: 24
wait...
Removed proc id: 9529
Child send: 12
wait...
wait...
*/

基于多任务的并发服务器

每当有客户端请求服务(连接请求)时,回声服务器端都创建子进程以提供服务。请求服务的客户端若有5个,则将创建5个子进程提供服务。为了完成任务,需要如下阶段:

  • 第一阶段: 回声服务器端(父进程)通过调用 accept 函数受理连接请求。

  • 第二阶段: 此时获取的套接字文件描述符创建并传递给子进程。

  • 第三阶段: 子进程利用传递来的文件描述符提供服务。

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;pid_t pid;struct sigaction act;socklen_t adr_sz;int str_len, state;char buf[BUF_SIZE];if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}// sigaction结构体初始化及信号处理函数注册act.sa_handler=read_childproc;sigemptyset(&act.sa_mask);act.sa_flags=0;state=sigaction(SIGCHLD, &act, 0);serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");while(1){adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);if(clnt_sock==-1)continue;elseputs("new client connected...");// 创建子进程pid=fork();if(pid==-1){close(clnt_sock);continue;}// 子进程运行if(pid==0){// 复制套接字后同一端口将对应多个套接字。调用fork函数后,要将无关的套接字文件描述符关掉,子进程关闭服务端套接字close(serv_sock);while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)write(clnt_sock, buf, str_len);close(clnt_sock);puts("client disconnected...");return 0;}else// 父进程关闭客户端连接套接字close(clnt_sock);}close(serv_sock);return 0;
}void read_childproc(int sig)
{pid_t pid;int status;pid=waitpid(-1, &status, WNOHANG);printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

I/O 分割的客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);int main(int argc, char *argv[])
{int sock;pid_t pid;char buf[BUF_SIZE];struct sockaddr_in serv_adr;if(argc!=3) {printf("Usage : %s <IP> <port>\n", argv[0]);exit(1);}sock=socket(PF_INET, SOCK_STREAM, 0);  memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=inet_addr(argv[1]);serv_adr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("connect() error!");// 子进程用于写数据,父进程用于读数据pid=fork();if(pid==0)write_routine(sock, buf);else read_routine(sock, buf);close(sock);return 0;
}void read_routine(int sock, char *buf)
{while(1){int str_len=read(sock, buf, BUF_SIZE);if(str_len==0)return;buf[str_len]=0;printf("Message from server: %s", buf);}
}
void write_routine(int sock, char *buf)
{while(1){fgets(buf, BUF_SIZE, stdin);if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n")){ shutdown(sock, SHUT_WR);return;}write(sock, buf, strlen(buf));}
}
void error_handling(char *message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

五、基于I/O复用的服务器端

创建进程需要大量的运算和内存空间,不同进程间的数据交换也相对复杂,因此 I/O复用技术就应运而生。

select 函数

#include <sys/select.h>
#include <sys/time . h>int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);
// 成功时返回大于0的值,失败时返回-1。
/*maxfd         监视对象文件描述符数量。readset         将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。writeset        将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。exceptset      将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,并传递其地址值。timeout            调用select函数后,为防止陷入无限阻塞的状态,传递超时( time-out )信息。返回值            发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
*/

使用select函数时可以将多个文件描述符集中到一起统一监视。select函数的调用方法和顺序如下图

设置文件描述符利用 fd_set 数组变量完成,在 fd_set 变量中注册或更改值的操作都由下列宏完成:

  • FD_ZERO(fd_set * fdset) : 将 fd_set 变量的所有位初始化为0 。

  • FD_SET(int fd, fd_set * fdset): 在参数 fdset 指向的变量中注册文件描述符 fd 的信息。

  • FD_ CLR(int fd, fd_set * fdset): 从参数 fdset 指向的变量中清除文件描述符fd的信息。

  • FD_ISSET(int fd, fd_set * fdset) : 若参数fdset指向的变量中包含文件描述符fd的信息, 则返回真。

设置检查(监视)范围及超时:

  • 文件描述符的监视范围与 select 函数的第一个参数有关。只需将最大的文件描述符值加1再传递到 select 函数即可。

  • select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下

struct timeval
{long tv_sec;   // secondslong tv_usec;  // microseconds
}

调用 select 函数后查看结果

select 函数调用完成后,向其传递的 fd_set 变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外,因此可以认为值仍为1 的位置上的文件描述符发生了变化。

select 函数调用示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>#define BUF_SIZE 30int main(int argc, char *argv[])
{fd_set reads, temps;int result, str_len;char buf[BUF_SIZE];struct timeval timeout;// 设置文件描述符FD_ZERO(&reads);FD_SET(0, &reads); // 0 is standard input(console)/*timeout.tv_sec=5;timeout.tv_usec=5000;*/while(1){temps=reads;// 设置超时时间timeout.tv_sec=5;timeout.tv_usec=0;// 调用select函数result=select(1, &temps, 0, 0, &timeout);if(result==-1){puts("select() error!");break;}else if(result==0){puts("Time-out!");}else {// 验证发生变化的文件描述符是否为标准输入,若是,则从标准输入读取数据并向控制台输出。if(FD_ISSET(0, &temps)) {str_len=read(0, buf, BUF_SIZE);buf[str_len]=0;printf("message from console: %s", buf);}}}return 0;
}
/*
root@my_linux:/tcpip# gcc select.c -o select
root@my_linux:/tcpip# ./select
Hi-
message from console: Hi-
Hello-
message from console: Hello-
Time-out!
Time-out!
Good bye-
message from console: Good bye-
*/

实现 I/O复用服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>#define BUF_SIZE 100
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;struct timeval timeout;fd_set reads, cpy_reads;socklen_t adr_sz;int fd_max, str_len, fd_num, i;char buf[BUF_SIZE];if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");FD_ZERO(&reads);          // 初始化文件描述符FD_SET(serv_sock, &reads);   // 注册服务器端套接字fd_max=serv_sock;while(1){cpy_reads=reads;timeout.tv_sec=5;timeout.tv_usec=5000;// 调用select函数if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)break;if(fd_num==0)continue;// 查找发生状态变化的文件描述符for(i=0; i<fd_max+1; i++){if(FD_ISSET(i, &cpy_reads)){// 判断是否为服务器端套接字if(i==serv_sock)     // connection request!{adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);FD_SET(clnt_sock, &reads);if(fd_max<clnt_sock)fd_max=clnt_sock;printf("connected client: %d \n", clnt_sock);}else    // read message!{str_len=read(i, buf, BUF_SIZE);if(str_len==0)    // close request!{FD_CLR(i, &reads);close(i);printf("closed client: %d \n", i);}else{write(i, buf, str_len);    // echo!}}}}}close(serv_sock);return 0;
}void error_handling(char *buf)
{fputs(buf, stderr);fputc('\n', stderr);exit(1);
}

优于 select 的 epoll

基于select 的 I/O复用技术速度慢的原因:

  • 调用 select 函数后常见的针对所有文件描述符的循环语句。

  • 每次调用 select 函数时都需要向该函数传递监视对象信息。

实现 epoll 所必要的函数和结构体

  • epoll_create 向操作系统请求创建保存文件描述符的空间

#include <sys/epoll.h>int epoll_create(int size);
// 成功时返回epoll文件描述符,失败时返回-1。
  • epoll_ctl 生成 epoll 例程后,应在其内部注册监视对象文件描述符

#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
// 成功时返回0 ,失败时返回-1
/*epfd  用于注册监视对象的epoll例程的文件描述符。op       用于指定监视对象的添加、删除或更改等操作。EPOLL_CTL_ADD  EPOLL_CTL_DEL  EPOLL_CTL_MOD  fd        需要注册的监视对象文件描述符。event    监视对象的事件类型。EPOLLIN: 需要读取数据的情况。EPOLLOUT: 输出缓冲为空,可以立即发送数据的情况。EPOLLPRI: 收到OOB数据的情况。EPOLLRDHUP: 断开连接或半关闭的情况,这在边缘触发方式下非常有用。EPOLLERR: 发生错误的情况。EPOLLET: 以边缘触发的方式得到事件通知。EPOLLONESHOT: 发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递                          EPOLL_CTL_MOD,再次设置事件。
*/
  • epoll_wait 与 select 函数对应

#include <sys/epoll.h>int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
// 成功时返回发生事件的文件描述符数,失败时返回-1
/*epfd      表示事件发生监视范围的epoll例程的文件描述符。events     保存发生事件的文件描述符集合的结构体地址值。maxevents 第二个参数中可以保存的最大事件数。timeout        以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
*/

基于 epoll 的回声服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");epfd=epoll_create(EPOLL_SIZE);ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);event.events=EPOLLIN;event.data.fd=serv_sock;   epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);if(event_cnt==-1){puts("epoll_wait() error");break;}for(i=0; i<event_cnt; i++){if(ep_events[i].data.fd==serv_sock){adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);event.events=EPOLLIN;event.data.fd=clnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);printf("connected client: %d \n", clnt_sock);}else{str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len==0)    // close request!{epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);close(ep_events[i].data.fd);printf("closed client: %d \n", ep_events[i].data.fd);}else{write(ep_events[i].data.fd, buf, str_len);    // echo!}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *buf)
{fputs(buf, stderr);fputc('\n', stderr);exit(1);
}

条件触发和边缘触发

条件触发方式中, 只要输入缓冲中还剩有数据,就将以事件方式再次注册。epoll 默认以条件触发方式工作。

边缘触发中输入缓冲收到数据时仅注册1 次该事件。即使输入缓冲中还留有数据,也不会再进行注册。

基于边缘触发的回声服务器端

边缘触发方式中,接收数据时仅注册1次该事件,因此需要验证输入缓冲是否为空;边缘触发方式下,以阻塞方式工作的 read & write 函数有可能引起服务器端的长时间停顿,因此需要采用非阻塞方式。

将套接字改为非阻塞方式的方法:

#include <fcntl.h>int fcntl(int filedes, int cmd, . . . );
// 成功时返回cmd参数相关值,失败时返回-1
/*filedes       属性更改目标的文件描述符。cmd            表示函数调用的目的。
*/// 将文件(套接字)改为非阻塞模式,如下两句
// int flag = fcntl(fd, F_GETFL, 0);
// fcntl(fd, F_SETFL, flag|O_NONBLOCK);

以边缘触发方式工作的回声服务器端示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>#define BUF_SIZE 4      //为了验证边缘触发的工作方式,将缓冲设置为4字节。
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");epfd=epoll_create(EPOLL_SIZE);ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);setnonblockingmode(serv_sock);        // 将套接字改为非阻塞模式event.events=EPOLLIN;event.data.fd=serv_sock;   epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);if(event_cnt==-1){puts("epoll_wait() error");break;}puts("return epoll_wait");      // 为观察事件发生数而添加的输出字符串的语句。for(i=0; i<event_cnt; i++){if(ep_events[i].data.fd==serv_sock){adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);setnonblockingmode(clnt_sock);       // 将套接字改为非阻塞模式event.events=EPOLLIN|EPOLLET;        // 添加EPOLLET标志,将套接字事件注册方式改为边缘触发。event.data.fd=clnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);printf("connected client: %d \n", clnt_sock);}else{while(1){str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len==0)    // close request!{epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);close(ep_events[i].data.fd);printf("closed client: %d \n", ep_events[i].data.fd);break;}else if(str_len<0){if(errno==EAGAIN)break;}else{write(ep_events[i].data.fd, buf, str_len);    // echo!}}}}}close(serv_sock);close(epfd);return 0;
}void setnonblockingmode(int fd)
{int flag=fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{fputs(buf, stderr);fputc('\n', stderr);exit(1);
}

边缘触发方式可以分离接收数据和处理数据的时间点,从实现模型的角度看,边缘触发更有可能带来高性能。

六、多线程服务器端

线程

为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程( Thread )。相比于进程有如下优点:

  • 线程的创建和上下文切换比进程的创建和上下文切换更快。

  • 线程间交换数据时无需特殊技术。

多个线程共享数据区和堆区,如果说进程在操作系统内部生成多个执行流,那么线程就在同一进程内部创建多条执行流。

线程的创建和执行

线程创建函数

#include <pthread.h>int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
// 成功时返回0, 失败时返回其他值。
/*thread            保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的IDattr           用于传递线程属性的参数,传递NULL时,创建默认属性的线程。start_routine    相当于线程main 函数的飞在单独执行流中执行的函数地址值(函数指针) 。arg               通过第三个参数传递调用函数时包含传递参数信息的变量地址值。
*/

如何准确预测出 thread_main 函数的运行时间,并让 mian 函数恰好等待这么长时间呢?通常利用下面的函数控制线程的执行流。

#include <pthread.h>int pthread_join(pthread_t thread, void **status);
// 成功时返回0,失败时返回其他值。
/*thread        该参数值ID的线程终止后才会从该函数返回。status     保存线程的main函数返回值的指针变量地址值。
*/

创建多个线程示例,下图为程序执行流程图

#include <stdio.h>
#include <pthread.h>
void * thread_summation(void * arg); int sum=0;int main(int argc, char *argv[])
{pthread_t id_t1, id_t2;int range1[]={1, 5};int range2[]={6, 10};pthread_create(&id_t1, NULL, thread_summation, (void *)range1);pthread_create(&id_t2, NULL, thread_summation, (void *)range2);pthread_join(id_t1, NULL);pthread_join(id_t2, NULL);printf("result: %d \n", sum);return 0;
}void * thread_summation(void * arg)
{int start=((int*)arg)[0];int end=((int*)arg)[1];while(start<=end){sum+=start;start++;}return NULL;
}

线程的问题和临界区

多个线程访问同一变量时存在同步的问题。

  • 2个线程同时执行函数,会构成临界区。

  • 2个线程分别执行不同的函数,也有可能构成临界区。

线程同步

  • 互斥量:不允许多个线程同时访问,通过加锁的方式

#include <pthread.h>
// 互斥量的创建和销毁
int pthread_mutex_init (pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
int pthread_mutex_destroy (pthread_mutex_t * mutex);
// 成功时返回0,失败时返回其他值。// 互斥量锁住和释放临界区
int pthread_mutex_lock (pthread_mutex_t * mutex);
int pthread_mutex_unlock (pthread_mutex_t * mutex); // 如果忘记调用则会产生死锁
// 成功时返回0,失败时返回其他值。
  • 信号量:利用"二进制信号量" ( 只用0和1 )完成"控制线程顺序"为中心的同步方法

#include <semaphore.h>int sem_init (sem_t * sem, int pshared, unsigned int value);
int sem_destroy (sem_ t * sem);
// 成功时返回0,失败时返回其他值。int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
// 成功时返回0,失败时返回其他值。
// 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1

线程销毁

之前调用过 pthread_join 函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通常通过如下函数调用引导线程销毁。

#include <pthread.h>int pthread_detach(pthread_t thread);
// 成功时返回0,失败时返回其他值。

多线程并发服务器端

这里介绍多个客户端之间可以交换信息的简单的聊天程序

服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>#define BUF_SIZE 100
#define MAX_CLNT 256void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;int clnt_adr_sz;pthread_t t_id;if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}pthread_mutex_init(&mutx, NULL);serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET; serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 5)==-1)error_handling("listen() error");while(1){clnt_adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);pthread_mutex_lock(&mutx);clnt_socks[clnt_cnt++]=clnt_sock;pthread_mutex_unlock(&mutx);pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);pthread_detach(t_id);printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));}close(serv_sock);return 0;
}void * handle_clnt(void * arg)
{int clnt_sock=*((int*)arg);int str_len=0, i;char msg[BUF_SIZE];while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)send_msg(msg, str_len);pthread_mutex_lock(&mutx);for(i=0; i<clnt_cnt; i++)   // remove disconnected client{if(clnt_sock==clnt_socks[i]){while(i++<clnt_cnt-1)clnt_socks[i]=clnt_socks[i+1];break;}}clnt_cnt--;pthread_mutex_unlock(&mutx);close(clnt_sock);return NULL;
}
void send_msg(char * msg, int len)   // send to all
{int i;pthread_mutex_lock(&mutx);for(i=0; i<clnt_cnt; i++)write(clnt_socks[i], msg, len);pthread_mutex_unlock(&mutx);
}
void error_handling(char * msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 100
#define NAME_SIZE 20void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];int main(int argc, char *argv[])
{int sock;struct sockaddr_in serv_addr;pthread_t snd_thread, rcv_thread;void * thread_return;if(argc!=4) {printf("Usage : %s <IP> <port> <name>\n", argv[0]);exit(1);}sprintf(name, "[%s]", argv[3]);sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=inet_addr(argv[1]);serv_addr.sin_port=htons(atoi(argv[2]));if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)error_handling("connect() error");pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);pthread_join(snd_thread, &thread_return);pthread_join(rcv_thread, &thread_return);close(sock);  return 0;
}void * send_msg(void * arg)   // send thread main
{int sock=*((int*)arg);char name_msg[NAME_SIZE+BUF_SIZE];while(1) {fgets(msg, BUF_SIZE, stdin);if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")) {close(sock);exit(0);}sprintf(name_msg,"%s %s", name, msg);write(sock, name_msg, strlen(name_msg));}return NULL;
}void * recv_msg(void * arg)   // read thread main
{int sock=*((int*)arg);char name_msg[NAME_SIZE+BUF_SIZE];int str_len;while(1){str_len=read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);if(str_len==-1) return (void*)-1;name_msg[str_len]=0;fputs(name_msg, stdout);}return NULL;
}void error_handling(char *msg)
{fputs(msg, stderr);fputc('\n', stderr);exit(1);
}

七、简易HTTP服务器端

HTTP

Web服务器:基于 HTTP协议,将网页对应文件传输给客户端的服务器端。

HTTP协议:HTTP 是以超文本传输为目的而设计的应用层协议。

  • 请求消息的结构

  • 响应消息的结构

常见状态码:

  • 200 OK:成功处理了请求!

  • 404 Not Found:请求的文件不存在!

  • 400 Bad Request:请求方式错误,请检查!

基于多线程的Web服务器端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 1024
#define SMALL_BUF 100void* request_handler(void* arg);
void send_data(FILE* fp, char* ct, char* file_name);
char* content_type(char* file);
void send_error(FILE* fp);
void error_handling(char* message);int main(int argc, char *argv[])
{int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;int clnt_adr_size;char buf[BUF_SIZE];pthread_t t_id;    if(argc!=2) {printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port = htons(atoi(argv[1]));if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock, 20)==-1)error_handling("listen() error");while(1){clnt_adr_size=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size);printf("Connection Request : %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port));pthread_create(&t_id, NULL, request_handler, &clnt_sock);pthread_detach(t_id);}close(serv_sock);return 0;
}void* request_handler(void *arg)
{int clnt_sock=*((int*)arg);char req_line[SMALL_BUF];FILE* clnt_read;FILE* clnt_write;char method[10];char ct[15];char file_name[30];clnt_read=fdopen(clnt_sock, "r");clnt_write=fdopen(dup(clnt_sock), "w");fgets(req_line, SMALL_BUF, clnt_read);  if(strstr(req_line, "HTTP/")==NULL){send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return;}strcpy(method, strtok(req_line, " /"));strcpy(file_name, strtok(NULL, " /"));strcpy(ct, content_type(file_name));if(strcmp(method, "GET")!=0){send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return;}fclose(clnt_read);send_data(clnt_write, ct, file_name);
}void send_data(FILE* fp, char* ct, char* file_name)
{char protocol[]="HTTP/1.0 200 OK\r\n";char server[]="Server:Linux Web Server \r\n";char cnt_len[]="Content-length:2048\r\n";char cnt_type[SMALL_BUF];char buf[BUF_SIZE];FILE* send_file;sprintf(cnt_type, "Content-type:%s\r\n\r\n", ct);send_file=fopen(file_name, "r");if(send_file==NULL){send_error(fp);return;}/* 传输头信息 */fputs(protocol, fp);fputs(server, fp);fputs(cnt_len, fp);fputs(cnt_type, fp);/* 传输请求数据 */while(fgets(buf, BUF_SIZE, send_file)!=NULL) {fputs(buf, fp);fflush(fp);}fflush(fp);fclose(fp);
}char* content_type(char* file)
{char extension[SMALL_BUF];char file_name[SMALL_BUF];strcpy(file_name, file);strtok(file_name, ".");strcpy(extension, strtok(NULL, "."));if(!strcmp(extension, "html")||!strcmp(extension, "htm")) return "text/html";elsereturn "text/plain";
}void send_error(FILE* fp)
{   char protocol[]="HTTP/1.0 400 Bad Request\r\n";char server[]="Server:Linux Web Server \r\n";char cnt_len[]="Content-length:2048\r\n";char cnt_type[]="Content-type:text/html\r\n\r\n";char content[]="<html><head><title>NETWORK</title></head>""<body><font size=+5><br>发生错误!查看请求文件名和请求方式""</font></body></html>";fputs(protocol, fp);fputs(server, fp);fputs(cnt_len, fp);fputs(cnt_type, fp);fflush(fp);
}void error_handling(char* message)
{fputs(message, stderr);fputc('\n', stderr);exit(1);
}

通过虚拟机运行服务器端代码,利用8000号端口提供 HTTP 服务

gcc webserv_linux.c -D_REENTRANT -o wserv -lpthread
./ wserv 8000

在虚拟机的浏览器中访问网址 http://127.0.0.1:8000/index.html 或 http://localhost:8000/index.html (需要将 html 文件放到服务器端代码同级目录下)

总结

通过《TCP/IP 网络编程》这本书,初步了解了网络编程,对网络编程有了大致的了解,为后续阅读《Linux 高性能服务器编程》并完成webserver 项目打下基础。

《TCP/IP 网络编程》笔记相关推荐

  1. 《信贷的逻辑与常识》笔记

    序 银行信贷风险管理的反思 现状与趋势 银行贷款的质量变化与经济周期.宏观调控政策等存在很高的相关性 现在银行不良贷款的增加主要是前几年经济快速增长时企业过度投资.银行过度放贷所带来的结果. 从历史情 ...

  2. AI公开课:19.02.27周逵(投资人)《AI时代的投资逻辑》课堂笔记以及个人感悟

    AI公开课:19.02.27周逵(投资人)<AI时代的投资逻辑>课堂笔记以及个人感悟 目录 课堂PPT图片 精彩语录 个人感悟 课堂PPT图片 精彩语录 更新中-- 文件图片已经丢失-- ...

  3. 人工智能入门算法逻辑回归学习笔记

    逻辑回归是一个非常经典的算法,其中也包含了非常多的细节,曾看到一句话:如果面试官问你熟悉哪个机器学习模型,可以说 SVM,但千万别说 LR,因为细节真的太多了. 秉持着精益求精的工匠精神不断对笔记进行 ...

  4. 【逻辑回归学习笔记】

    算法描述 1.逻辑回归要做的事就是寻找分界面实现二分类. 2.问题假设:对一堆三角形和正方形分类. 3.数据输入:已知正方形和三角形的坐标和标签. 4.算法过程: 知识储备 1.分类和回归 ①分类的目 ...

  5. 逻辑回归函数学习笔记

    继续逻辑回归学习,今日笔记记录. 1.逻辑回归和线性回归的关系:对逻辑回归的概率比取自然对数,则得到的是一个线性函数,推导过程如下. 首先,看逻辑回归的定义 其次,计算两个极端y/(1-y),其值为( ...

  6. 2.2 逻辑回归-机器学习笔记-斯坦福吴恩达教授

    逻辑回归 上一节我们知道,使用线性回归来处理 0/1 分类问题总是困难重重的,因此,人们定义了逻辑回归来完成 0/1 分类问题,逻辑一词也代表了是(1) 和 非(0). Sigmoid预测函数 在逻辑 ...

  7. LVM逻辑卷分区笔记

    磁盘的静态分区有其缺点:分区大小难评估,估计不准确,当分区空间不够用的时候,系统管理员可能需要先备份整个系统,清除磁盘空间,然后重新对磁盘进行分区,然后恢复磁盘数据到新分区,且需要停机一段时间进行恢复 ...

  8. 适合理工直男的钟平老师逻辑英语学习笔记

    一切的一切都只是套路!             --鲁迅 核心公式: En: (状语1) 主(定语1) 谓(状语2) (宾)(定语2) (状语1) Ch: (状语1) (定语1)主 (状语2)谓 (定 ...

  9. 【数字逻辑】学习笔记 第四章 Part2 常用组合逻辑电路与竞争、险象

    文章目录 一.常用组合逻辑电路 1. 译码器 (1) 二进制译码器 74LS138(3/8译码器) a. 一般符号和图形符号 b. 74LS138功能表 c. 两片 `74LS138` 构成 `4-1 ...

  10. 线性回归、逻辑回归学习笔记

    学习源代码 import numpy as np import matplotlib.pyplot as plt def true_fun(X): # 这是我们设定的真实函数,即ground trut ...

最新文章

  1. javascript + css 利用div的scroll属性让TAB动感十足
  2. 研发项目进度管理软件正式上线
  3. 构建之法第二章读后感
  4. 《动物森友会》的社交分级,在虚拟世界设计舒适的社交氛围
  5. 大学生试用期辞退之没有工钱
  6. Jenkins命令可视化
  7. 前端基础入门四(JavaScript基础)
  8. php复制xml文件,PHP_php xml文件操作实现代码(二),复制代码 代码如下:?php //创 - phpStudy...
  9. 软件工程学习笔记《二》代码规范
  10. Tomcat免安装版的环境变量配置以及Eclipse下的Tomcat配置和测试
  11. 【LeetCode笔记】26. 删除有序数组中的重复项(Java、原地)
  12. linux /etc/passwd文件各参数的意义
  13. 用Java控制小电灯-树莓派PI4J
  14. 大数据_Flink_数据处理_案例WorldCount_批处理版---Flink工作笔记0008
  15. python中文视频教程-鱼C零基础入门 Python中文视频教程
  16. header简单用处
  17. ai/ml_本月有关AI / ML的令人印象深刻的中等文章
  18. 视频教程-CCNA视频----从零开始学CCNA实验视频课程(加强版)-思科认证
  19. utils 16进制HEX 与 rgba互转
  20. 商务网站建设与维护【9】

热门文章

  1. RMVB转换成家用DVD可播放的格式
  2. 后台管理界面-- 管理员管理[3]
  3. 使用PortTunnel来透过局域网架网站
  4. java计算机毕业设计红河旅游信息服务系统源程序+mysql+系统+lw文档+远程调试
  5. 麻城筹资5亿打造智慧城市 建成信息化应用系统
  6. 最新产品 L1频段卫星导航射频前端低噪声放大器芯片AT2659
  7. linux 下的光盘拷贝
  8. 球面距离计算方式(杭州到各城市的球面距离计算球面距离)
  9. 机械设备公司有什么与计算机相关的职位,机械设计制造及其自动化专业就业从事什么职位...
  10. 产品经理实习生面试or工作常问问题(个人的总结)