下图说明了TCP服务端与客户端的实现顺序,对于整个流程还有疑问的、或者不太理解的可以翻阅理解网络编程与套接字,我们把整个过程比作了打电话,便于大家理解。其中 close(),read(),write()函数我们在第一节基于Linux的文件操作有简单的讲解与实现,而剩下的socket(),bind(),listen(),accept(),connect()函数在网络编程各基础函数的使用方法有详细的说明。本节TCP服务端与客户端的实现本质是把各函数揉在了一起。
我们只需要把六个调用过程实现就行,如果还有疑问请在上篇查阅各函数的参数类型使用方法。
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
char message[] = "Hello world!";//要发送的信息
//创建套接字socket()
int serv_sock;
serv_sock = socket(AF_INET, SOCK_STREAM, 0);
//分配ip地址与端口号
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr);
//进入等待连接请求状态listen()
listen(serv_sock, 5) ;
//受理客户端请求accept()
int clnt_sock;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
//write与close
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
上述服务器端可以完成但是网络编程的函数往往有返回值告知是否调用正确,我们增加程序的鲁棒性,提示在哪里出错并结束运行,此外,把变量的定义放在最前面。
#include
#include
#include
#include
#include
#include
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello world!";
//调用格式是传入的可执行文件以及端口号
if (argc != 2)//传入两个参数正确,其他结束
{
printf("Usage: %s \n" , argv[0]);
exit(1);
}
//创建套接字socket()
serv_sock = socket(AF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) //新增
error_handling("sock() error");
//分配ip地址与端口号
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));//第二个参数就是端口号
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error"); //新增
//进入等待连接请求状态listen()
if (listen(serv_sock, 5) == -1)
error_handling("listen() error"); //新增
//受理客户端请求accept()
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1) //新增
error_handling("accept() error");
//write与close
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message) //新增处理函数
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
char message[30];
//创建套接字socket()
int sock;
sock = socket(AF_INET, SOCK_STREAM, 0);
//connect()
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr);
//read()
int str_len;
str_len = read(sock, message, sizeof(message) - 1);
printf("Message from server: %s\n", message);
//close()
close(sock);
return 0;
}
同样对客户端也进行修改
#include
#include
#include
#include
#include
#include
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
//调用格式是传入的可执行文件、ip地址、端口号
if (argc != 3)/传入三个参数正确,其他结束
{
printf("Usage: %s \n" , argv[0]);
exit(1);
}
//创建套接字socket()
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("sock() error");
//connect()
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);//第二个参数是ip地址
serv_addr.sin_port = htons(atoi(argv[2]));//第三个参数是端口号
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
//read()
str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
error_handling("read() error!");
printf("Message from server: %s\n", message);
//close()
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
打开两个终端,linux命令如下
//新建hello_server.c文件
touch hello_server.c
//编辑(敲代码)
vim hello_server.c
//编译
gcc hello_server.c -o hserver
//运行
./hserver 9190
//新建hello_client.c文件
touch hello_client.c
//编辑(敲代码)
vim client_server.c
//编译
gcc client_server.c -o hclient
//运行
./hclient 127.0.0.1 9190
回声服务器端与客户端顾名思义,服务器端将客户端传输的字符串数据原封不动地传回客户端,就像回声一样。上面讨论的hello_word服务器端处理完一个客户端连接请求退出,那么该怎么修改呢、最好的办法是插入循环语句反复调用accept函数。
基本运作方式
新增循环语句实现上图的流程
#define BUF_SIZE 1024
char message[BUF_SIZE];
clnt_adr_sz = sizeof(clnt_adr);
for(int i = 0;i < 5;i++)//调用5次
{
//accept() 注意第二参数是客户端不是服务端、第三个参数是二参结构体长度的指针
clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
if(clnt_sock == -1)
error_handling("accept() error");
else
printf("Connect client %d \n",i+1);//输出这是第几个连接
//read()、write()
while((str_len = read(clnt_sock,message,BUF_SIZE)) != 0)
write(clnt_sock,message,str_len);//回写
//close client
close(clnt_sock);
}
//close server
close(serv_sock);
完整echo_server.c
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char* argv[])
{
int serv_sock,clnt_sock;
int str_len;//读取的字节数
char message[BUF_SIZE];
struct sockaddr_in serv_adr,clnt_adr ;
socklen_t clnt_adr_sz;
if(argc != 2)
{
printf("Usage : %s \n" ,argv[0]);
exit(1);
}
serv_sock = socket(PF_INET,SOCK_STREAM,0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));//端口号
if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
error_handling("bind() error");
if(listen(serv_sock,5) == -1)
error_handling("listen() error");
clnt_adr_sz = sizeof(clnt_adr);
for(int i = 0;i < 5;i++)//调用5次
{
//accept()
clnt_sock = accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
if(clnt_sock == -1)
error_handling("accept() error");
else
printf("Connect client %d \n",i+1);//输出这是第几个连接
//read()、write()
while((str_len = read(clnt_sock,message,BUF_SIZE)) != 0)
write(clnt_sock,message,str_len);//回写
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端可以发送无限次数的消息,直到输入Q、q退出,那么我们需要用while(1)循环完成,输入Q、q再break就行。但是发送无限次数不等于一次发送无线长度的消息,发送长度需要小于BUF_SIZE。
#define BUF_SIZE 1024
char message[BUF_SIZE];
while(1)
{
fputs("Input Message(Q to quit): ",stdout); //输出
fgets(message,BUF_SIZE,stdin); //输入
//输入q、Q直接退出
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock,message,strlen(message));
str_len = read(sock,message,BUF_SIZE - 1); //去掉字符串结束符
message[str_len] = 0; //字符串结束符位置覆盖掉
printf("Message from server: %s",message); //回显发送的信息
}
close(sock);
完整echo_client.c
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char* argv[])
{
int sock;
int str_len;//读取的字节数
char message[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage : %s \n" ,argv[0]);
exit(1);
}
serv_sock = socket(PF_INET,SOCK_STREAM,0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));//端口号
if(connect(sock,(struct sockaddr_in*)&serv_adr,sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("connected......");
while(1)
{
fputs("Input Message(Q to quit): ",stdout); //输出
fgets(message,BUF_SIZE,stdin); //输入
//输入q、Q直接退出
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock,message,strlen(message));
str_len = read(sock,message,BUF_SIZE - 1); //去掉字符串结束符
message[str_len] = 0; //字符串结束符位置覆盖掉
printf("Message from server: %s",message); //回显发送的信息
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
先运行服务器端,再打开客户端。不断发送消息再退出,可以看到服务器端会显示已经连接的次数,第五次结束后程序退出
echo_client.c仅仅只调用一次read函数,而TCP是不存在数据边界的。如果数据量太大,客户端可能未收到全部数据包时就调用read()函数,因此我们可以改成循环调用read()函数。
while(1)
{
fputs("Input Message(Q to quit): ",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
str_len=write(sock,message,strlen(message));
recv_len = 0;
while(recv_len < str_len)
{
recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s",message);
}
记录写入的字节数,如果已经read()的字节小于写入的字节就继续read()。完整代码如下,在数据量不大时运行结果和未修改的客户端结果相同,再次不赘述
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char* argv[])
{
int sock;
int str_len,recv_len,recv_cnt;
char message[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage : %s \n" ,argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_STREAM,0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));//端口号
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
error_handling("connect() error");
else
puts("connected......");
while(1)
{
fputs("Input Message(Q to quit): ",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
str_len=write(sock,message,strlen(message));
recv_len = 0;
while(recv_len < str_len)
{
recv_cnt = read(sock,&message[recv_len],BUF_SIZE - 1);
if(recv_cnt == -1)
error_handling("read() error");
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}