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

 

posted @ 2019-02-23 21:29  丶丨zuoluo  阅读(240)  评论(0编辑  收藏  举报