我的认为《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.\n"); 31 str_echo(connfd); 32 exit(0); 33 } 34 } 35 36 printf("server end!\n"); 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", 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\n"); 55 } 56 } 57 }
编译:gcc server.c -o serversocket
这里先列下常常用到的网络字段类型:tcp
代码流程:函数
一、申请socketspa
服务器首先申请socket,socket相似于再Unix系统上打开一个文件,会返回一个文件标识号用来标识当前打开的文件。指针
socket须要引用<sys/socket.h>头文件code
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
二、端口绑定
通常服务器启动一个服务进程会开启某个端口的监听工做,因此通常的服务器进程须要绑定固定的端口号,也就是该进程对应的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:为套接字结构体长度
三、套接字端口监听
目前已经在申请好的套接字上进行了监听ip及port的初始化,那么能够内核开始按照咱们初始化的信息进行监听了,即调用listen函数,内核会申请一个队列用于存放未完成链接以及已完成链接的套接字,以下图
映射到tcp的三次握手,以下图:
四、与客户端创建链接
下边咱们会进入一个无限循环,会一直处理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\n"); 40 } 41 fputs(recvline, stdout); 42 bzero(recvline, MAXLINE); 43 } 44 }
编译:gcc client.c -o client
客户端的流程:
一、创建套接字
二、发起tcp链接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
connect也是阻塞函数,tcp链接成功后返回0。
到这里咱们完成了一个超级简单的服务器-客户端程序的开发。以后咱们会对这个程序不断完善。
下文:
本篇中写的服务器,fork的子进程执行完直接调exit了,咱们知道子进程结束后可是父进程没有回收其对应的空间(进程号等),随着子进程的不停申请,但得不到释放,内核会内存泄露,也就是变成了僵尸进程。下一篇,咱们引入对子进程的空间释放解决这个问题。