基本TCP socket编程

来源:互联网 发布:网络安全技术专业书目 编辑:程序博客网 时间:2024/04/29 13:15

基本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来处理每个客户连接的服务器并发执行的。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 超市柜子纸丢了怎么办 楼下有污水井味道怎么办 孕妇闻到了烧垃圾怎么办 楼下9米垃圾房怎么办 在工厂上班得了职业病怎么办 自来水被农药水污染了怎么办 雾霾天头疼恶心怎么办? 夫妻住宾馆一个没有身份证怎么办 医保报销后认定工伤怎么办 结肠癌术后复查有息肉怎么办 无蒂息肉恶变要怎么办 贤者时间很长怎么办 鸡吃了酒米醉了怎么办 自填脂肪乳房脂肪液化怎么办 中国人在外国遇到危险怎么办 dnf刷图卡住了怎么办 dbf深渊怪卡住了怎么办 dnf86级没任务了怎么办 dnf二觉任务没了怎么办 脚趾甲变空向上翘怎么办 汽油车加了一点柴油怎么办 柴油车辆环保检测功率不足怎么办 加95加错一次92怎么办 新车95加错92油怎么办 加不到95号汽油怎么办 去新疆没95号油怎么办 黄龙300加了92怎么办 gla错加92号油 怎么办 95和98混加了怎么办 沥青车可以停在居民区怎么办 汽油进到眼睛了怎么办 汽油进了眼睛里怎么办 眼睛里面进了汽油怎么办 脱硫塔里的二氧化硫高怎么办 恐怖黎明铁匠选错怎么办 堡垒之夜草变色怎么办 火柴没有擦的了怎么办 乙醚倒进下水道了怎么办 乙醚和水不分层怎么办 乙醚闻多了头晕怎么办 爱乐维吃了便秘怎么办