unix网络编程(1)---客户端-服务器初版

我的认为《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了,咱们知道子进程结束后可是父进程没有回收其对应的空间(进程号等),随着子进程的不停申请,但得不到释放,内核会内存泄露,也就是变成了僵尸进程。下一篇,咱们引入对子进程的空间释放解决这个问题。

相关文章
相关标签/搜索