问题:实现服务端和客户端之间的TCP通讯。
大致的过程如下:
服务端:
1、创建TCP套接字(socket)
2、设置socket的选项,即对socket进行设置
3、服务端的设置,包括协议族,在socket编程中只能是AF_INET,存储端口号等
4、将一本地地址与一套接口捆绑。绑定到一个IP地址和一个端口上
5、服务端进行监听,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开。
6、监听之后,若是能够收到客户端的连接时则创建一个用于和连接进来的客户端进行通信的socket(socketConnection)
7、即续监听,等侍下一个客户的连接
客户端:
1、用域名或主机名获取IP地址
2、建立套接字。
3、设置待访问服务端信息
4、建立连接(指明IP地址和端口号)
5、发送数据
6、接受数据
大致的过程用图表示如下:
服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开。客户端创建socket ,客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket。服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求【TCP服务器监听到客户端请求之后,调用accept()函数取接收请求】,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端请求。客户端连接成功,向服务器发送连接状态信息,服务器accept方法返回,连接成功。客户端向socket写入信息,服务器读取信息,客户端关闭,服务器端关闭。
代码:
1)服务端代码:
//服务端代码
#include <sys/time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 4321
#define BACKLOG 1
#define MAXRECVLEN 1024
int main(int argc, char *argv[])
{
char buf[MAXRECVLEN];
int listenfd, connectfd; /* socket descriptors */
struct sockaddr_in server; /* server's address information */
struct sockaddr_in client; /* client's address information */
socklen_t addrlen;
/* Create TCP socket */
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
/* handle exception */
perror("socket() error. Failed to initiate a socket");
exit(1);
}
/* set socket option */
int opt = SO_REUSEADDR;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
// sockfd:标识一个套接口的描述字。
// level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
// optname:需设置的选项。
// optval:指针,指向存放选项待设置的新值的缓冲区。
// optlen:optval缓冲区长度。
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{//将一本地地址与一套接口捆绑
// sockfd 表示已经建立的socket编号(描述符);
// my_addr 是一个指向sockaddr结构体类型的指针;
// 参数addrlen表示my_addr结构的长度
/* handle exception */
perror("Bind() error.");
exit(1);
}
if(listen(listenfd, BACKLOG) == -1)
{// 套接字与“地方”绑定好了后,对于服务端而言(这里是TCP情况),聆听客户端的需求
// 第一个参数是服务端套接字,第二个参数是等待连接队列的最大长度
//成功返回0, 失败返回-1.
perror("listen() error. \n");
exit(1);
}
addrlen = sizeof(client);
while(1){
if((connectfd=accept(listenfd,(struct sockaddr *)&client, &addrlen))==-1)
{
// accept函数指定服务端去接受客户端的连接,接收后,
// 返回了客户端套接字的标识,且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。
perror("accept() error. \n");
exit(1);
}
struct timeval tv;
gettimeofday(&tv, NULL);//获得当前精确时间
printf("You got a connection from client's ip %s, port %d at time %ld.%ld\n",inet_ntoa(client.sin_addr),htons(client.sin_port), tv.tv_sec,tv.tv_usec);
//对此输出的地址client.sin_addr有所疑问,并不会随着客户端的输入的变化而变化?
//这是因为从ifconfig中可以看出lo为环回接口,它的IP地址固定为127.0.0.1,掩码8位。它代表你的机器本身。
//inet_ntoa将一个IP转换成一个互联网标准点分格式的字符串。
int iret=-1;
while(1)
{
iret = recv(connectfd, buf, MAXRECVLEN, 0);
if(iret>0)
{
printf("%s\n", buf);
}else
{
close(connectfd);
break;
}
/* print client's ip and port */
send(connectfd, buf, iret, 0); /* send to the client welcome message */
}
}
close(listenfd); /* close listenfd */
return 0;
}
注意:
通常服务器在启动的时候都会通过bind绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
2)客户端代码:
//客户端代码
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h> /* netdb is necessary for struct hostent */
#define PORT 4321 /* server port */
#define MAXDATASIZE 100
int main(int argc, char *argv[])
{
int sockfd, num; /* files descriptors */
char buf[MAXDATASIZE]; /* buf will store received text */
struct hostent *he; /* structure that will get information about remote host */
struct sockaddr_in server;
if (argc != 2)
{
printf("Usage: %s <IP Address>\n",argv[0]);
exit(1);
}
if((he=gethostbyname(argv[1]))==NULL)//返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针
{
printf("gethostbyname() error\n");
exit(1);
}
if((sockfd=socket(AF_INET,SOCK_STREAM, 0))==-1)
{
printf("socket() error\n");
exit(1);
}
bzero(&server,sizeof(server));
server.sin_family = AF_INET;//指代协议族,在socket编程中只能是AF_INET
server.sin_port = htons(PORT);//存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,
//同时0~1024范围的端口号已经被系统使用或保留
server.sin_addr = *((struct in_addr *)he->h_addr);//存储IP地址,使用in_addr这个数据结构
if(connect(sockfd, (struct sockaddr *)&server, sizeof(server))==-1)//connect用于建立与指定外部端口socket的连接
{
// 函数原型: int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR* name, int namelen);
// 参数:
// s:标识一个未连接socket
// name:指向要连接套接字的sockaddr结构体的指针
// namelen:sockaddr结构体的字节长度
printf("connect() error\n");
exit(1);
};
char str[100]="I am liujiepeng";//在
if((num=send(sockfd,str,sizeof(str),0))==-1){
//int send( SOCKET s, const char FAR *buf, int len, int flags );
//不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,
//而服务器则通常用send函数来向客户程序发送应答。
//该函数的第一个参数指定发送端套接字描述符;
//第二个参数指明一个存放应用程序要发送数据的缓冲区;
//第三个参数指明实际要发送的数据的字节数;
//第四个参数一般置0。
printf("send() error\n");
exit(1);
}
if((num=recv(sockfd,buf,MAXDATASIZE,0))==-1)
{
// 不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
// 该函数的第一个参数指定接收端套接字描述符;
// 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
// 第三个参数指明buf的长度;
// 第四个参数一般置0。
printf("recv() error\n");
exit(1);
}
buf[num-1]='\0';
printf("server message: %s\n",buf);
close(sockfd);
return 0;
}
编译运行:
以上两个程序放在同一个目录下,比如 /home/liujiepng/Public/tcpCSmodel
命令行进入该目录 $ cd /home/liujiepng/Public/tcpCSmodel
命令行执行 $ gcc -o client client.c ,可以编译出客户端程序。
命令行执行 $ gcc -o server server.c,可以编译出服务端程序。
命令行执行 $ ./server,启动server程序。得先启动服务端的程序,否则客服端会产生链接错误的:
再启动服务端之后,重新打开一个命令行窗口,到刚才的目录下,执行 $ ./client 127.0.0.1,启动客户端程序,就可以看到结果了。
本程序客户端会自动退出,服务器不会,因此如果想停掉服务器程序,直接在命令行界面按键盘Ctrl+C停止。程序实现的功能很简单,就是服务器监听4321端口,客户端与之建立TCP连接后,再发送字符串“I am liujiepeng”到服务端,服务端打印出来,然后再把字符串传回给客户端,客户端再打印出来。然后客户端关闭连接退出,而服务端继续监听4321端口等待下一次连接。
左图是服务端,右图是客户端。
如果我们更改下客户端的输入,产生的结果如下所示:
127开头的客户端都是可以作为客户端的输入的,而其他的如128打头的ip则会产生连接错误。从ifconfig可以看出,eth0是网卡信息,lo为环回接口,它的IP地址固定为127.0.0.1,掩码8位,代表你的机器本身。所以,127是网络地址,其掩码为255.0.0.0。所以,只要在同一网络下的客户端发起的请求才可以被响应。下图中的128网络则连接失败。