socket的tcp协议连接服务器,socket编程-- 基于TCP协议的网络程序

可以看到server程序监听8000端口,IP地址还没确定下来。现在编译运行客户端:

$ ./client abcd

Response from server:

ABCD

回到server所在的终端,看看server的输出:

$ ./server

Accepting connections ...

received from 127.0.0.1 at PORT 59757

可见客户端的端口号是自动分配的。现在把客户端所连接的服务器IP改为其它主机的IP,试试两台主机的通讯。

再做一个小实验,在客户端的connect()代码之后插一个while(1);死循环,使客户端和服务器都处于连接中的状态,用netstat命令查看:

$ ./server &

[1] 8343

$ Accepting connections ...

./client abcd &

[2] 8344

$ netstat -apn|grep 8000

tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8343/server

tcp 0 0 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED8344/client

tcp 0 0 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED8343/server

应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。

表中 . client和server的socket状态

socket文件描述符源地址:源端口号目的地址:目的端口号状态server.c中的listenfd0.0.0.0:80000.0.0.0:*LISTEN

server.c中的connfd127.0.0.1:8000127.0.0.1:44406ESTABLISHED

client.c中的sockfd127.0.0.1:44406127.0.0.1:8000ESTABLISHED

错误处理与读写控制

上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。

为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:

#include

#include

#include

void perr_exit(const char *s)

{

perror(s);

exit(1);

}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)

{

int n;

again:

if ( (n = accept(fd, sa, salenptr)) < 0) {

if ((errno == ECONNABORTED) || (errno == EINTR))

goto again;

else

perr_exit("accept error");

}

return n;

}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)

{

if (bind(fd, sa, salen) < 0)

perr_exit("bind error");

}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)

{

if (connect(fd, sa, salen) < 0)

perr_exit("connect error");

}

void Listen(int fd, int backlog)

{

if (listen(fd, backlog) < 0)

perr_exit("listen error");

}

int Socket(int family, int type, int protocol)

{

int n;

if ( (n = socket(family, type, protocol)) < 0)

perr_exit("socket error");

return n;

}

ssize_t Read(int fd, void *ptr, size_t nbytes)

{

ssize_t n;

again:

if ( (n = read(fd, ptr, nbytes)) == -1) {

if (errno == EINTR)

goto again;

else

return -1;

}

return n;

}

ssize_t Write(int fd, const void *ptr, size_t nbytes)

{

ssize_t n;

again:

if ( (n = write(fd, ptr, nbytes)) == -1) {

if (errno == EINTR)

goto again;

else

return -1;

}

return n;

}

void Close(int fd)

{

if (close(fd) == -1)

perr_exit("close error");

}

慢系统调用accept、read和write被信号中断时应该重试。connect虽然也会阻塞,但是被信号中断时不能立刻重试。对于accept,如果errno是ECONNABORTED,也应该重试。详细解释见参考资料。

TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用,如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回,但如果socket文件描述符有O_NONBLOCK标志,则write不阻塞,直接返回20。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,也放在wrap.c中:

ssize_t Readn(int fd, void *vptr, size_t n)

{

size_t nleft;

ssize_t nread;

char *ptr;

ptr = vptr;

nleft = n;

while (nleft > 0) {

if ( (nread = read(fd, ptr, nleft)) < 0) {

if (errno == EINTR)

nread = 0;

else

return -1;

} else if (nread == 0)

break;

nleft -= nread;

ptr += nread;

}

return n - nleft;

}

ssize_t Writen(int fd, const void *vptr, size_t n)

{

size_t nleft;

ssize_t nwritten;

const char *ptr;

ptr = vptr;

nleft = n;

while (nleft > 0) {

if ( (nwritten = write(fd, ptr, nleft)) <= 0) {

if (nwritten < 0 && errno == EINTR)

nwritten = 0;

else

return -1;

}

nleft -= nwritten;

ptr += nwritten;

}

return n;

}

如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户端上传文件的协议,规定前12字节表示文件名,超过12字节的文件名截断,不足12字节的文件名用'\0'补齐,从第13字节开始是文件内容,上传完所有文件内容后关闭连接,服务器可以先调用readn读12个字节,根据文件名创建文件,然后在一个循环中调用read读文件内容并存盘,循环结束的条件是read返回0。

字段长度固定的协议往往不够灵活,难以适应新的变化。比如,以前DOS的文件名是8字节主文件名加“.”加3字节扩展名,不超过12字节,但是现代操作系统的文件名可以长得多,12字节就不够用了。那么制定一个新版本的协议规定文件名字段为256字节怎么样?这样又造成很大的浪费,因为大多数文件名都很短,需要用大量的'\0'补齐256字节,而且新版本的协议和老版本的程序无法兼容,如果已经有很多人在用老版本的程序了,会造成遵循新协议的程序与老版本程序的互操作性(Interoperability)问题。如果新版本的协议要添加新的字段,比如规定前12字节是文件名,从13到16字节是文件类型说明,从第17字节开始才是文件内容,同样会造成和老版本的程序无法兼容的问题。

现在重新看看上一节的TFTP协议是如何避免上述问题的:TFTP协议的各字段是可变长的,以'\0'为分隔符,文件名可以任意长,再看blksize等几个选项字段,TFTP协议并没有规定从第m字节到第n字节是blksize的值,而是把选项的描述信息“blksize”与它的值“512”一起做成一个可变长的字段,这样,以后添加新的选项仍然可以和老版本的程序兼容(老版本的程序只要忽略不认识的选项就行了)。

因此,常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行的比用'\0'的更常见,例如本节后面要介绍的HTTP协议。可变长字段的协议用readn来读就很不方便了,为此我们实现一个类似于fgets的readline函数,也放在wrap.c中:

static ssize_t my_read(int fd, char *ptr)

{

static int read_cnt;

static char *read_ptr;

static char read_buf[100];

if (read_cnt <= 0) {

again:

if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {

if (errno == EINTR)

goto again;

return -1;

} else if (read_cnt == 0)

return 0;

read_ptr = read_buf;

}

read_cnt--;

*ptr = *read_ptr++;

return 1;

}

ssize_t Readline(int fd, void *vptr, size_t maxlen)

{

ssize_t n, rc;

char c, *ptr;

ptr = vptr;

for (n = 1; n < maxlen; n++) {

if ( (rc = my_read(fd, &c)) == 1) {

*ptr++ = c;

if (c == '\n')

break;

} else if (rc == 0) {

*ptr = 0;

return n - 1;

} else

return -1;

}

*ptr = 0;

return n;

}

2.3. 把client改为交互式输入

目前实现的client每次运行只能从命令行读取一个字符串发给服务器,再从服务器收回来,现在我们把它改成交互式的,不断从终端接受用户输入并和server交互。

client.c

#include

#include

#include

#include

#include "wrap.h"

#define MAXLINE 80

#define SERV_PORT 8000

int main(int argc, char *argv[])

{

struct sockaddr_in servaddr;

char buf[MAXLINE];

int sockfd, n;

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

servaddr.sin_port = htons(SERV_PORT);

Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

while (fgets(buf, MAXLINE, stdin) != NULL) {

Write(sockfd, buf, strlen(buf));

n = Read(sockfd, buf, MAXLINE);

if (n == 0)

printf("the other side has been closed.\n");

else

Write(STDOUT_FILENO, buf, n);

}

Close(sockfd);

return 0;

}这时server仍在运行,但是client的运行结果并不正确。原因是什么呢?仔细查看server.c可以发现,server对每个请求只处理一次,应答后就关闭连接,client不能继续使用这个连接发送数据。但是client下次循环时又调用write发数据给server,write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错,而server收到数据后应答一个RST段,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。client下次循环又调用write发数据给server,由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序,所以看到上面的现象。

为了避免client异常退出,上面的代码应该在判断对方关闭了连接后break出循环,而不是继续write。另外,有时候代码中需要连续多次调用write,可能还来不及调用read得知对方已关闭了连接就被SIGPIPE信号终止掉了,这就需要在初始化时调用sigaction处理SIGPIPE信号,如果SIGPIPE信号没有导致进程异常退出,write返回-1并且errno为EPIPE。

另外,我们需要修改server,使它可以多次处理同一客户端的请求。server.cc

#include

#include

#include

#include "wrap.h"

#define MAXLINE 80

#define SERV_PORT 8000

int main(void)

{

struct sockaddr_in servaddr, cliaddr;

socklen_t cliaddr_len;

int listenfd, connfd;

char buf[MAXLINE];

char str[INET_ADDRSTRLEN];

int i, n;

listenfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(SERV_PORT);

Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

Listen(listenfd, 20);

printf("Accepting connections ...\n");

while (1) {

cliaddr_len = sizeof(cliaddr);

connfd = Accept(listenfd,

(struct sockaddr *)&cliaddr, &cliaddr_len);

while (1) {

n = Read(connfd, buf, MAXLINE);

if (n == 0) {

printf("the other side has been closed.\n");

break;

}

printf("received from %s at PORT %d\n",

inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),

ntohs(cliaddr.sin_port));

for (i = 0; i < n; i++)

buf[i] = toupper(buf[i]);

Write(connfd, buf, n);

}

Close(connfd);

}

}

经过上面的修改后,客户端和服务器可以进行多次交互了。我们知道,服务器通常是要同时服务多个客户端的,运行上面的server和client之后,再开一个终端运行client试试,新的client能得到服务吗?想想为什么。

2.4. 使用fork并发处理多个client的请求

怎么解决这个问题?网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。

以下给出代码框架,完整的代码请读者自己完成。

listenfd = socket(...);

bind(listenfd, ...);

listen(listenfd, ...);

while (1) {

connfd = accept(listenfd, ...);

n = fork();

if (n == -1) {

perror("call to fork");

exit(1);

} else if (n == 0) {

close(listenfd);

while (1) {

read(connfd, ...);

...

write(connfd, ...);

}

close(connfd);

exit(0);

} else

close(connfd);

}

2.5. setsockopt

现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:$ ./server

bind error: Address already in use

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:$ netstat -apn |grep 8000

tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client

tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -

server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。

现在用Ctrl-C把client也终止掉,再观察现象:$ netstat -apn |grep 8000

tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -

$ ./server

bind error: Address already in use

client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间请读者参考UNP 2.7节。

在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()调用之间插入如下代码:int opt = 1;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

有关setsockopt可以设置的其它选项请参考UNP第7章。

2.6. 使用select

select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server。

/* server.c */

#include

#include

#include

#include

#include "wrap.h"

#define MAXLINE 80

#define SERV_PORT 8000

int main(int argc, char **argv)

{

int i, maxi, maxfd, listenfd, connfd, sockfd;

int nready, client[FD_SETSIZE];

ssize_t n;

fd_set rset, allset;

char buf[MAXLINE];

char str[INET_ADDRSTRLEN];

socklen_t cliaddr_len;

struct sockaddr_in cliaddr, servaddr;

listenfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(SERV_PORT);

Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

Listen(listenfd, 20);

maxfd = listenfd; /* initialize */

maxi = -1; /* index into client[] array */

for (i = 0; i < FD_SETSIZE; i++)

client[i] = -1; /* -1 indicates available entry */

FD_ZERO(&allset);

FD_SET(listenfd, &allset);

for ( ; ; ) {

rset = allset; /* structure assignment */

nready = select(maxfd+1, &rset, NULL, NULL, NULL);

if (nready < 0)

perr_exit("select error");

if (FD_ISSET(listenfd, &rset)) { /* new client connection */

cliaddr_len = sizeof(cliaddr);

connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

printf("received from %s at PORT %d\n",

inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),

ntohs(cliaddr.sin_port));

for (i = 0; i < FD_SETSIZE; i++)

if (client[i] < 0) {

client[i] = connfd; /* save descriptor */

break;

}

if (i == FD_SETSIZE) {

fputs("too many clients\n", stderr);

exit(1);

}

FD_SET(connfd, &allset); /* add new descriptor to set */

if (connfd > maxfd)

maxfd = connfd; /* for select */

if (i > maxi)

maxi = i; /* max index in client[] array */

if (--nready == 0)

continue; /* no more readable descriptors */

}

for (i = 0; i <= maxi; i++) { /* check all clients for data */

if ( (sockfd = client[i]) < 0)

continue;

if (FD_ISSET(sockfd, &rset)) {

if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {

/* connection closed by client */

Close(sockfd);

FD_CLR(sockfd, &allset);

client[i] = -1;

} else {

int j;

for (j = 0; j < n; j++)

buf[j] = toupper(buf[j]);

Write(sockfd, buf, n);

}

if (--nready == 0)

break; /* no more readable descriptors */

}

}

}

}

select

/*

* server.cc

*

* Created on: 2012-1-17

* Author: simondu

*/

#include "head.h"

#define MAXLINE 80

#define SERV_PORT 8000

int main(void )

{

int i, maxi, maxfd, listenfd, connfd, sockfd;

int nready, client[FD_SETSIZE];

ssize_t n;

fd_set rset, allset;

char buf[MAXLINE];

char str[INET_ADDRSTRLEN];

socklen_t cliaddr_len;

struct sockaddr_in    cliaddr, servaddr;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(SERV_PORT);

bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

listen(listenfd, 20);

maxfd = listenfd;        /* initialize */

maxi = -1;            /* index into client[] array */

for (i = 0; i < FD_SETSIZE; i++)

client[i] = -1;    /* -1 indicates available entry */

FD_ZERO(&allset);

FD_SET(listenfd, &allset);

printf("Starting ... \n");

for ( ; ; ) {

rset = allset;    /* structure assignment */

nready = select(maxfd+1, &rset, NULL, NULL, NULL);

if (nready < 0)

{

printf("select error");

exit(1);

}

if (FD_ISSET(listenfd, &rset)) { /* new client connection */

cliaddr_len = sizeof(cliaddr);

connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

printf("received from %s at PORT %d\n",

inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),

ntohs(cliaddr.sin_port));

for (i = 0; i < FD_SETSIZE; i++)

if (client[i] < 0) {

client[i] = connfd; /* save descriptor */

break;

}

if (i == FD_SETSIZE) {

fputs("too many clients\n", stderr);

exit(1);

}

FD_SET(connfd, &allset);    /* add new descriptor to set */

if (connfd > maxfd)

maxfd = connfd; /* for select */

if (i > maxi)

maxi = i;    /* max index in client[] array */

if (--nready == 0)

continue;    /* no more readable descriptors */

}

for (i = 0; i <= maxi; i++) {    /* check all clients for data */

if ( (sockfd = client[i]) < 0)

{

printf("sockfd = client[i] = %d\n",sockfd);

continue;

}

if (FD_ISSET(sockfd, &rset)) {

if ( (n = read(sockfd, buf, MAXLINE)) == 0) {

/* connection closed by client */

close(sockfd);

FD_CLR(sockfd, &allset);

client[i] = -1;

} else {

int j;

for (j = 0; j < n; j++)

buf[j] = toupper(buf[j]);

int rettest = write(sockfd, buf, n);

printf("%d\n",rettest);

}

if (--nready == 0)

break;    /* no more readable descriptors */

}

}

}

}

test  \\\\

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值