个人认为《Unix网络编程》前4章可以好好看几遍,不用先着急编程。另外作者提供的源码封装太重,不如自己基于原始库函数编写客户端以及服务器,目前一些开源的项目也都是基于这些基础库函数的。
在了解了前四章的主要知识点后,比如socket、bind、connect、listen、accept等函数后,对网络编程有了一定的了解后,就可以参考第5章来写自己的客户端和服务器了。对于新手来说这里比较抽象,而且很多地方绕来绕去容易绕晕,需要重复看多次,再看后边的章节。
这篇文章我就从第5章开始,仿照书上的demo写一个可以直接在单机上运行的cli-ser程序。
以下是server的对应程序:server.c
1 #include <unistd.h> 2 #include <stdlib.h> 3 #include <errno.h> 4 5 #define MAXLINE 1024 6 7 extern int errno; 8 9 void str_echo(int); 10 11 int main() { 12 int sockfd; 13 sockfd = socket(AF_INET, SOCK_STREAM, 0); 14 15 struct sockaddr_in servaddr, cliaddr; 16 bzero(&servaddr, sizeof(servaddr)); 17 servaddr.sin_family = AF_INET; 18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 19 servaddr.sin_port = htons(7070); 20 bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); 21 listen(sockfd, 1024); 22 23 for (;;) { 24 int connfd, childPid; 25 socklen_t len = sizeof(cliaddr); 26 connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); 27 28 if ((childPid = fork()) == 0) { 29 close(sockfd); 30 printf("connected with client. "); 31 str_echo(connfd); 32 exit(0); 33 } 34 } 35 36 printf("server end! "); 37 return 0; 38 } 39 40 void str_echo(int sockfd) { 41 ssize_t n; 42 char buf[MAXLINE]; 43 44 again: 45 46 while ((n = read(sockfd, buf, MAXLINE)) > 0) { 47 printf("n:%ld ", n); 48 write(sockfd, buf, n); 49 bzero(buf, MAXLINE); 50 51 if (n < 0 && errno == EINTR) { 52 goto again; 53 } else if (n < 0) { 54 printf("str_echo:read error "); 55 } 56 } 57 }
编译:gcc server.c -o server
这里先列下经常用到的网络字段类型:
代码流程:
1、申请socket
服务器首先申请socket,socket类似于再Unix系统上打开一个文件,会返回一个文件标识号用来标识当前打开的文件。
socket需要引用<sys/socket.h>头文件
int socket(int family, int type, int protocol);
family:对应的是协议族,ipv4:AF_INET ipv6:AF_INET6
type:套接字类型,tcp对应SOCKET_STREAM(数据流)
protocol:协议类型,这里我们用0,内核会根据family和type选择默认的协议,对于family:AF_INET,type:SOCK_STREAM,默认的协议是tcp
2、端口绑定
一般服务器启动一个服务进程会开启某个端口的监听工作,所以一般的服务器进程需要绑定固定的端口号,也就是该进程对应的socket需要绑定到某一个端口号。对于多网卡的服务器,会对应多个ip,当然也可以绑定固定的ip,我们这里不进行绑定 ,使用通配地址(ipv4:INADDR_ANY,ipv6:IN6ADDR_ANY_INIT),此处的端口或ip绑定用的函数是bind
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd:监听套接字,对于服务器来说,即调socket返回的套接字
myaddr:套接字结构体,我们一般会先申请一个sockaddr_in结构的套接字,通过bzero函数(string.h的一个函数)进行结构体初始化为0,分别对family,ip,port填值,然后用sockaddr强制类型转化进行调用,具体的可以参考书中bind函数使用;
addrlen:为套接字结构体长度
3、套接字端口监听
目前已经在申请好的套接字上进行了监听ip及port的初始化,那么可以内核开始按照我们初始化的信息进行监听了,即调用listen函数,内核会申请一个队列用于存放未完成连接以及已完成连接的套接字,如下图
映射到tcp的三次握手,如下图:
4、与客户端建立连接
下边我们会进入一个无限循环,会一直处理client发来的tcp链接,accept为阻塞函数,如果没有客户端连接,这个函数会被阻塞,也就是程序会在这里停止,知道有client建立了tcp连接accept才返回,accept返回也就说明,此时已经建立好一条tcp连接通路,下边我们的服务器会在这条通路上进行数据的发送与接收,至于接收后会怎么处理,以及返回客户端什么数据,就属于服务器自己的业务需求了。我们这里会fork一个子进程进行这些逻辑的处理。为什么要建立子进程呢?我们的服务器进程是并发的服务器,如果accept后,进程开始处理业务逻辑,那么其他的client需要等待这条tcp完成逻辑处理后,才能进入下一次循环。所以我们新建子进程专门用于逻辑的处理,至于父进程就专门负责accept,建立新的链接,这样多个client发起与服务器的tcp链接,服务器主进程可以一直循环accept建立连接,然后fork子进程进行后续处理,这样我们就实现了简单的并发服务器,可以同时与多个client建立tcp连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
这里有一点需要注意,addrlen使用的是指针,这是由于addrlen的入参会被内核使用到,已提醒读取cliaddr的长度,另外,内核会写回cliaddr,这也防止内存溢出,并且写入多少这个数,内核还会写回addrlen,这里一个参数做了多个事情,所以用了值—参数这种指针传参。
#include <unistd.h>
pid_t fork(void);
创建子进程,对于父进程返回值为子进程的进程id,对于子进程返回0。
对于server中用到的read和write函数,参考Unix高级编程中的相关知识。
以下是client代码:client.c
1 #include <sys/socket.h> 2 #include <netinet/in.h> 3 #include <stdio.h> 4 #include <string.h> 5 #include <arpa/inet.h> 6 #include <unistd.h> 7 #include <unistd.h> 8 9 #define MAXLINE 1024 10 11 void str_cli(FILE *, int); 12 13 int main() { 14 int sockfd; 15 const char *ip = "127.0.0.1"; 16 in_port_t port = 7070; 17 18 int i = 0; 19 sockfd = socket(AF_INET, SOCK_STREAM, 0); 20 struct sockaddr_in cliaddr; 21 bzero(&cliaddr, sizeof(cliaddr)); 22 cliaddr.sin_family = AF_INET; 23 inet_aton(ip, &cliaddr.sin_addr); 24 cliaddr.sin_port = htons(port); 25 26 int ret = connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); 27 str_cli(stdin, sockfd); 28 29 return 0; 30 } 31 32 void str_cli(FILE *fp, int sockfd) { 33 char sendline[MAXLINE], recvline[MAXLINE]; 34 35 while (fgets(sendline, MAXLINE, fp) != NULL) { 36 write(sockfd, sendline, strlen(sendline)); 37 38 if (read(sockfd, recvline, MAXLINE) == 0) { 39 printf("server terminated prematurely "); 40 } 41 fputs(recvline, stdout); 42 bzero(recvline, MAXLINE); 43 } 44 }
编译:gcc client.c -o client
客户端的流程:
1、建立套接字
2、发起tcp连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
connect也是阻塞函数,tcp连接成功后返回0。
到这里我们完成了一个超级简单的服务器-客户端程序的开发。之后我们会对这个程序不断完善。
下文:
本篇中写的服务器,fork的子进程执行完直接调exit了,我们知道子进程结束后但是父进程没有回收其对应的空间(进程号等),随着子进程的不停申请,但得不到释放,内核会内存泄露,也就是变成了僵尸进程。下一篇,我们引入对子进程的空间释放解决这个问题。