TCP网络编程有三个例子最值得学习研究,分别是echo、chat、proxy,都是长连接协议。接下来,把这几个例子都实现。本节用一个简单的例子来讲TCP客户/服务器程序框架,这也是echo的实现。
程序的基本流程:
- 客户从标准输入键入一行文本,并发送给服务器。
- 服务器接收到文本之后回射给客户端。
- 客户端接收到服务器的文本,把它显示到标准输出上。
尽管下列实现代码很简单,但是它已经阐述了基本的tcp客户/服务器的框架,想要实现任何复杂的程序都可以以这个程序作为基本框架来开发。比如做成一问一答的方式,收到的请求和发送响应的内容不一样,这时候要考虑打包与拆包格式的设计,进一步还可以写简单的HTTP服务。以下贴出源码。后续以tcp socket的框架写个简单功能的聊天室。
服务器程序
//server_echo.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#define SERV_PORT 9877
#define LISTENQ 1024
#define MAXLINE 4096
void str_echo(int sockfd)
{
char buff[MAXLINE];
int length=0;
printf("server begin recv\n");
while(length=recv(sockfd,buff,MAXLINE,0)) //这里是分包接收,每次接收4096个字节
{
if(length<0)
{
perror("recv");
exit(-1);
}
printf("server send\n");
if (send(sockfd,buff,MAXLINE,0) < 0)
{
perror("Send");
exit(-1);
}
bzero(buff, sizeof(buff));
}
}
int main(int argc, char **argv)
{
int listenfd, connfd,fpid;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
//建立socket连接
if ((listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(1);
}
printf("create socket success!\n");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 设置套接字选项避免地址使用错误,为了允许地址重用,我设置整型参数(on)为 1 (不然,可以设为 0 来禁止地址重用)
int on=1;
if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)
{
perror("setsockopt failed");
exit(-1);
}
if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
{
perror("bind");
exit(-1);
}
printf("Bind success!\n");
if(listen(listenfd, LISTENQ) == -1)
{
perror("listen");
exit(-1);
}
for ( ; ; )
{
clilen = sizeof(cliaddr);
printf("begin accept!\n");
if ((connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) < 0)
{
perror("accept");
exit(-1);
}
printf("begin fork!\n");
fpid=fork();
if (fpid < 0)
{
perror("fork");
exit(-1);
}
else if (fpid == 0) //child process
{
close(listenfd); // close listening socket
str_echo(connfd); // process the request
exit(0);
}
close(connfd); // parent closes connected socket
}
}
客户端程序
//client_echo.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXLINE 4096
#define SERV_PORT 9877
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while(fgets(sendline, MAXLINE, fp) != NULL)
{
printf("sendline : %s\n",sendline);
if (send(sockfd,sendline,strlen(sendline),0) < 0)
{
perror("Send");
exit(-1);
}
if (recv(sockfd,recvline,MAXLINE,0) < 0 )
{
perror("recv");
exit(-1);
}
printf("recvline : %s\n",recvline);
fputs(recvline, stdout);
}
}
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
{
perror("usage: tcpcli <IPaddress>");
exit(-1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("sockfd");
exit(-1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0)
{
perror("inet_pton");
exit(-1);
}
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(-1);
}
str_cli(stdin, sockfd);
exit(0);
}
测试结果如下:
ubuntu:~/test/1214-test$ ./server_echo
create socket success!
Bind success!
begin accept!
begin fork!
begin accept!
server begin recv
server send
ubuntu:~/test/1214-test$ ./client_echo 192.168.65.1
helloworld
sendline : helloworld
recvline : helloworld
helloworld
调试问题:
在调试程序的时候,我发现如果服务器被断开了,再重新启动,会出现如下错误:
bind: Address already in use
查了之后,才知道这是由于套接字处于TIME_WAIT状态引起的,这个时间是几分钟,过后再重新启动服务器就没问题了。有时候,我们在调试程序的时候,为了允许地址重用,可以在bind前加上这两句话可以避免这种状况,设置整型参数 on 为 1 (不然,可以设为 0 来禁止地址重用)。
int on=1;
if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)
{
perror("setsockopt failed");
exit(-1);
}