基本TCP socket编程

基本TCP socket编程

1. 介绍

一个典型的TCP客户端和服务端的时间轴图标:

 

2. socket函数

为了执行网络I/O, 首先就是要调用socket函数来,来声明我们连接协议类型(使用IPv4,IPv6等)。

#include <sys/socket.h>

int socket (int family, int type, int protocol);

Returns: non-negative descriptor if OK, -1 on error

 

family指定了协议类型

type

protocol

 

不是所有的family和type的组合都是有效的,下图显示了有效的组合

 

当socket函数调用成功之后,返回一个非负整数值,类似一个文件描述符,我们称之为套接字描述符,sickfd。我们只需要指定family和type参数来获得socket描述符。

 

3. connect函数

connect 函数用来从TCP客户端与TCP 服务器建立连接。

#include<sys/socket.h>

intconnect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

Returns:0 if OK, -1 on error

 

sockfd参数是之前调用socket函数返回的套接字描述符。第二个和第三个参数是socket地址结构体的指针和大小。这个结构体指针必须包含IP地址和服务器的端口号,下面先看一个例子,来自参考书第一章中的daytime。

intro/daytimetcpcli.c

 

 1 #include  "unp.h"

 

 2 int

 3 main(int argc,char **argv)

 4 {

 5     int    sockfd, n;

 6     char   recvline[MAXLINE + 1];

 7     struct sockaddr_in servaddr;

 

 8     if (argc != 2)

 9         err_quit("usage: a.out<IPaddress>");

 

10     if ( (sockfd= socket(AF_INET, SOCK_STREAM, 0)) < 0)

11         err_sys("socket error");

 

12    bzero(&servaddr, sizeof(servaddr));

13    servaddr.sin_family = AF_INET;

14    servaddr.sin_port = htons(13);  /*daytime server */

15     if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)

16        err_quit("inet_pton error for %s", argv[1]);

 

17     if(connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)

18         err_sys("connect error");

 

19     while ( (n =read(sockfd, recvline, MAXLINE)) > 0) {

20         recvline[n] = 0;        /* null terminate */

21         if(fputs(recvline, stdout) == EOF)

22            err_sys("fputs error");

23     }

24     if (n <0)

25         err_sys("read error");

 

26     exit(0);

27 }

 

connect函数初始化了TCP的3步握手,只有当连接被建立或者发生错误时,才被返回。有如下可能的错误发生:

a. 如果TCP客户端接受到没有回应,返回ETIMEDOUT。

b. 如果服务器给客户端的回应是一个RST,这表明了没有进程在服务端指定的端口号在等待建立连接。返回ECONNREFUSED。

c. 远端服务器没有收到返回EHOSTUNREACH 或者 ENETUNREACH。

我们可以使用上面的代码区测试一下一上的一些错误的发生。

 

4. bind函数

bind函数分配一个本地的协议地址给socket,根据互联网协议,地址是由一个32位的IPv4地址或者128位的IPv6地址组成,还有一个16位的TCP或者UDP端口号组成。

#include<sys/socket.h>

intbind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

Returns:0 if OK,-1 on error

第二个参数是制定协议地址的指针,第三个参数是这个地址结构体的大小。TCP协议中调用bind函数我们需要制定端口号,ip地址。

服务端要绑定一个已知的端口,如果tcp客户端或者服务端不选择一个端口,内核会提供一个短暂的端口给socket。

一个进程绑定一个制定的ip地址给套接口(socket),这个ip地址必须是属于一个主机的接口,对于一个tcp客户端来说,这个制定的ip地址源会被用来给ip数据包发送在套接口上。对于一个tcp服务端来说,这个ip地址会限制套接口来接受从客户端发来的连接。

以下列出了制定ip地址和端口号给bind之后返回的结果,

如果我们制定一个0端口号,内核会选择一个短暂的端口号来绑定。但是如果我们制定一个通配的ip地址,内核就不会选择本地的ip地址知道socket被建立起来。

这个通配的地址给bind函数,INADDR_ANY

        struct sockaddr_in   servaddr;

        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);     /* wildcard */

下面是一个daytime客户端使用bind的例子

intro/daytimetcpsrv.c

 1 #include     "unp.h".

 2 #include     <time.h>

 

 3 int

 4 main(intargc, char **argv)

 5 {

 6     int    listenfd, connfd;

 7     struct sockaddr_in servaddr;

 8     char   buff[MAXLINE];

 9     time_t ticks;

 

10     listenfd =Socket(AF_INET, SOCK_STREAM, 0);

 

11    bzeros(&servaddr, sizeof(servaddr));

12    servaddr.sin_family = AF_INET;

13    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

14    servaddr.sin_port = htons(13); /* daytime server */

 

15    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

 

16    Listen(listenfd, LISTENQ);

 

17     for ( ; ; ){

18         connfd =Accept(listenfd, (SA *) NULL, NULL);

 

19         ticks =time(NULL);

20        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

21        Write(connfd, buff, strlen(buff));

 

22        Close(connfd);

23     }

24 }

 

5.listen 函数

listen只被TCP server调用,listen执行2个动作:

1)当一个socket函数被调用来建立一个socket,代表一个socket被激活,也就是说客户端的socket会调用connect函数。函数listen将未连接的套接口转化成被动套接口,指示内核应接受指向此套接口的连接请求。如下图,调用listen函数使得套接口从closed状态转化到listen状态。

2)第二个参数是指定了内核为这个套接口最大的排队数,也就是最多的连接数。

#include<sys/socket.h>

#intlisten (int sockfd, int backlog);

Returns:0 if OK, -1 on error

 

这个函数一般在调用了socket和bind之后被调用,必须在accept函数被调用之前执行。

为了理解第二个参数backlog,我们必须意识到,作为一个正在监听的套接口,内核维护了2个队列:

1)未完成连接的队列(anincomplete connection queue)为每个这样的SYN分节开设一个条目:已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。这些套接口都处于SYN_RCVD状态。

2)已完成连接队列(acompleted connection queue)为每个已完成TCP三路握手过程的客户开设一个条目。这些套接口都处于ESTABLISHED状图,可参考上面的图。

监听套接口的这两个队列

当一个已完成连接队列被建立起来,从监听的套接口传过来的参数被copy到新建立的连接。建立连接的机制是自动完成的,服务端的进程没有被调用到。

2个队列建立连接时所交换的分组

当客户端发送一个SYN到服务端,TCP建立一个新的未完成连接队列,然后回应三路握手的第二个分节,也就是响应SYN,并且附带对客户SYN的ACK。这个连接将一直保持到未连接队列的第三个三路握手步骤到达,或者知道timeouterror。如果三路握手正常完成了,这个未完成连接队列会转向为已完成连接队列。当client进程执行accept函数,已完成队列中的对头条目返回给进程,当对列为空时,进程将睡眠,知道有条目放入已完成连接队列才唤醒它。

一个listen的封装函数

lib/wrapsock.c

 

137 void

138 Listen (int fd, int backlog)

139 {

140    char    *ptr;

 

141         /*can override 2nd argument with environment variable */

142     if ( (ptr =getenv("LISTENQ")) != NULL)

143         backlog= atoi (ptr);

 

144     if (listen(fd, backlog) < 0)

145         err_sys("listen error");

146 }

 

6.accept函数

当TCP服务端返回下一个从头部已完成连接队列的已完成连接是调用,如果已完成队列是空,那么进程将睡眠。

#include<sys/socket.h>

intaccept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

Returns:non-negative descriptor if OK, -1 on error

 cliaddr和addrlen这2个参数被用来返回客户端进程的协议地址。

如果accept调用成功,就会返回一个由内核自动生成的描述符。新的描述符是根据客户端的TCP连接而定的。我们把accept的返回值称为已连接套接口,存在于服务端的生命周期中。

这个函数返回3个可能的值:一个整型值,要么是新的套接口描述符,要么是一个错误值。如果我们对于客户端的协议地址没兴趣的话,可以把cliaddr和addrlen的指针都设置为NULL。

下面是一个实例,其中把第二个和第三个参数都不是NULL,然后我们借助于传进去的第二个和第三个参数来打印出客户端的ip地址和端口号。

intro/daytimetcpsrv1.c

 

 1 #include    "unp.h" 2

 2 #include    <time.h>

 

 3 int

 4 main(int argc,char **argv)

 5 {

 6     int    listenfd, connfd;

 7     socklen_t len;

 8     struct sockaddr_in servaddr, cliaddr;

 9     char   buff[MAXLINE];

10     time_t  ticks;

 

11     listenfd =Socket(AF_INET, SOCK_STREAM, 0);

 

12    bzero(&servaddr, sizeof(servaddr));

13    servaddr.sin_family = AF_INET;

14    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

15    servaddr.sin_port = htons(13);  /*daytime server */

 

16     Bind(listenfd,(SA *) &servaddr, sizeof(servaddr));

 

17    Listen(listenfd, LISTENQ);

 

18     for ( ; ; ){

19         len =sizeof(cliaddr);

20         connfd =Accept(listenfd, (SA *) &cliaddr, &len);

21        printf("connection from %s, port %d\n",

22               Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),

23               ntohs(cliaddr.sin_port));

 

24         ticks =time(NULL);

25        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

26        Write(connfd, buff, strlen(buff));

 

27        Close(connfd);

28     }

29 }

 

可以结合之前的server和client执行,会发现打印出client的ip地址和端口号,而且要注意的是,我们的server端程序,必须使用超级用户运行,因为只超级用户才能绑定预留的端口13,不然会发生Permission denied错误。

 

7. fork 和exec函数

在我们要讨论下一节中要讲到的写一个并发的server端程序之前,我们必须先要了解下UNIX的fork函数。在UNIX中这个只有这个方法来创建一个进程。

note:fork知识恶补:http://blog.csdn.net/zhangjie201412/article/details/7695159

#include<unistd.h>

pid_tfork(void);

Returns:0 in child, process ID of child in parent, -1 on error

 如果你之前从来都没有见过这个函数,理解fork函数的难点在于fork被调用的时候返回2个返回值。一个返回是当前进程(父进程)的进程ID,还在紫禁城中返回,返回值是0.所以,根据返回值判断进程是子进程还是父进程。

为什么在子进程中fork返回0呢,而不是父进程的进程ID呢?因为一个孩子只有一个父亲,他总是可以调用getppid来得到父进程的ID号。一个父亲,理论上来说可以拥有任意多个孩子,没有什么方法可以获得孩子的进程ID。如果一个父进程想要跟踪他的子进程,他必须记录下fork的返回值。

所有的文件描述符先要在父进程中被打开,然后再调用fork,在fork函数返回之后来共享这个描述符。这个特征被用到网络服务器总:父进程调用accept函数然后调用fork函数。已连接套接口在父进程和子进程中被共享。当然,子进程读写已连接套接口,父进程关闭这个套接口。

有2个fork的典型用法:

1)一个进程拷贝他自身,一个拷贝用来执行自己的操作,另外一个拷贝用来执行另外的任务。这个典型的用法用在网络服务器中,后面会讲到。

2)一个进程执行另外的一个程序。既然只有fork可以创建一个新的进程,那么进程首先调用fork来拷贝自身,一个拷贝(子进程)调用exec来待机自己执行新的程序。这个典型用法被用在shell中。

 

The execfunction

#include <unistd.h>

int execl (const char *pathname, const char *arg0, ... /* (char *) 0 */ );

int execv (const char *pathname, char *const argv[]);

int execle (const char *pathname, const char *arg0, ...

/* (char *) 0, char *const envp[] */ );

int execve (const char *pathname, char *const argv[], char *const envp[]);

int execlp (const char *filename, const char *arg0, ... /* (char *) 0 */ );

int execvp (const char *filename, char *const argv[]);

All six return: -1 on error, no return on success

 

这6个exec函数的不同之处:

a)  可执行文件被制定为一个文件名或者路径名

b)  新程序的参数是一一列出还是由一个指针数组来索引

c)  调用进程的环境传递给新程序还是指定新环境

下图为这6个函数之间的关系,一般来说,只有execve是一个内核的系统调用,其他5个都是调用execve的库函数。

 

8. 并发的server

在介绍accept的时候我们的例子是一个迭代的server。对于像例子这么简单的时间,这足够了。但是当一个客户端的要求占用很长的时间的话,就会有问题了。我们就不想要一个server对应一个client,我们需要的是多个client同时来访问。最简单的就是写一个并发的server没使用UNIX的fork函数来给每一个client创建一个子进程。

pid_t pid;

int  listenfd,  connfd;

 

listenfd = Socket( ... );

 

    /* fill insockaddr_in{} with server's well-known port */

Bind(listenfd, ... );

Listen(listenfd, LISTENQ);

 

for ( ; ; ) {

    connfd = Accept(listenfd, ... );    /* probably blocks*/

 

    if( (pid =Fork()) == 0) {

      Close(listenfd);    /* childcloses listening socket */

      doit(connfd);       /* process therequest */

      Close(connfd);      /* done withthis client */

      exit(0);            /* childterminates */

    }

 

   Close(connfd);         /* parentcloses connected socket */

}

当建立一个连接,accept返回,server调用fork,子进程处理客户端,父进程等待另一个连接。到子进程得到一个新的client之后父进程关闭已连接的socket。

下面我们来形象的描述上面那段代码

上图显示了client端的状态和server调用accept阻塞之后client去调用connect来请求连接。

Server调用accept之后返回connfd已连接套接口,现在可以进行数据的read/write了。

上图就是并发的server调用fork来创建进程。

这里要注意的是listenfd 和connfd 在子进程和父进程中被共享。

然后这里就是父进程关闭已连接套接口,紫禁城关闭监听套接口。

 

9. close 函数

#include <unistd.h>

int close (int sockfd);

Returns: 0 if OK, -1 on error

10. getsocketname 和 getpeername 函数

第一个函数返回的是本地协议地址,第二个返回的是远端的协议地址。

#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);

int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

Both return: 0 if OK, -1 on error

 

这两个函数被调用的地方:

a) 当client端connect函数被成功调用之后,getsocketname会返回本地的ip地址和本地的端口号。

b) 当使用0端口号调用bind函数之后,getsocketname返回本地端口号。

c) getsocketname被调用可以得到套接口的地址簇。(如下面的实例代码)

d) 在捆绑了一个通配IP地址的TCP服务器上,一旦与Client端建立了连接,就可以调用getsocketname函数来获得分配给这个连接的本地IP地址。在这样的调用中套接口描述子参数必须是已连接套接口描述子,而不是监听套接口描述子。

e) 当一个服务器调用accept的进程调用exec启动执行时,他获得clent身份的唯一途径就是调用getpeername。守护进程inetd fork和exec 一个TCP服务器时就是这么做的。

 

得到一个套接口地址簇实例

lib/sockfd_to_family.c

 1 #include    "unp.h"

 2 int

 3sockfd_to_family(int sockfd)

 4 {

 5     struct sockaddr_storage ss;

 6     socklen_t len;

 

 7     len = sizeof(ss);

 8     if (getsockname(sockfd, (SA *) &ss,&len) < 0)

 9         return (-1);

10     return(ss.ss_family);

11 }

 

 

11. 总结

所有的客户端和服务端都是从调用socket开始的,返回一个套接口描述符,然后client调用connect,server调用bind、listen和accept。套接口一般由标准的close函数关闭,当然可以调用shutdown。

多数TCP服务器是与调用fork来处理每个客户连接的服务器并发执行的。

你可能感兴趣的:(socket,tcp,server,struct,服务器,Descriptor)