本程序使用的TCP协议,该协议是面向连接、通过字节流进行通信的。实现了客户端和服务器端的阻塞式通信。主要锻炼了对于socket API的使用。
程序使用的函数
其中socket、bind、地址转换函数在网络基础编程-UDP为例已经分析过。这里介绍的函数适用于TCP这类面向连接的协议。
listen
当我们的网络程序需要使用TCP面向连接一类的协议的时候,socket中选用了SOCK_STREAM选项。此时需要我们的服务器端进入listen监听状态,用来监听客户端的连接。
//让服务器进入listen监听状态
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功返回0,失败返回-1
参数
backlog指的是连接队列,将未进入连接的请求处于排序状态。比如现在客户端A已经连接中,此时客户端B想要进行连接的时候,如果服务器端没法提供服务,就将客户端B放入连接等待队列。
该参数不能设置的太大,也不能设置的太小。太大就会浪费服务器端资源。太小就会让处于连接等待的客户端数量减小。如果连接队列已经满了,就会忽略后来的链接请求。
accept
accept函数在服务器端调用了listen函数之后,进行连接等待。
//接受客户端的连接请求。
#include <sys/socket.h>
int accept(int sockfd, struct sockadrr* addr, socklent_t* len);
//成功返回文件描述符,失败返回-1
参数
sockfd是用来socket函数返回的套接字;addr是用来存放连接客户端的地址数据;len存放连接客户端的addr的大小。
注意:
accept函数返回了一个文件描述符,这个文件描述符要和socket返回的文件描述符区分开来。 socket的fd是用来建立连接使用的,之后我们使用了listen函数将它变成了用来监听的套接字;accept返回的是请求连接的客户端的文件描述符,该文件描述符用来进行数据传输。
返回的accept_fd和socket函数的socket_fd有着同样的套接字类型和地址族。如果服务器端并不关心客户端的地址和长度,可以将accept函数的后两个参数设置为NULL。
如果没有连接进入的时候,accept默认会阻塞等待。但是我们可以通过让套接字进入非阻塞状态,这样accept会返回-1,同时将errno设置为EAGAIN。本程序使用阻塞式。
connect
//客户端通过该函数向服务器端发起连接请求。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t len);
//成功返回0,失败返回-1
参数
sockfd是socket函数返回的文件描述符;addr是服务器端的地址族;len是addr的大小。
send
该函数跟write基本一样,就是多了一个flags选项。同时send通常用于面向连接的协议,如当前的TCP。用于UDP的是sendto函数,但是TCP也可以使用sendto函数。
#include <sys/types.h>
#include <sys/socket.h>
//用来数据发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//成功返回发送的字节数,失败返回-1
参数
sockfd是写入发送数据的文件描述符;buf是发送数据的缓冲区;len是发送的字节数;flags是标志位,通过他可以实现面向连接协议的一些特定功能,通过我们让flags为0,表示不使用。这样功能和write就是一样的。
注意
就算send函数成功返回,也不代表客户端成功接收,只是说明该数据已经成功进入了当前的网络驱动中。
recv
#include <sys/types.h>
#include <sys/socket.h>
//用来数据接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//成功返回数据的字节数。如果没有可用数据或者对等方已经按序结束,返回0;如果出错返回-1
recv的参数和send一样,只是flags的选项有些不一样。
代码
服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
//定义服务器IP地址
#define SERVICE_IP INADDR_ANY
//定义listen等待队列
#define WAIT_QUEUE 5
// ./service port
int main(int argc, char* argv[]){
if(argc != 2){
printf("Usage: %s port\n", argv[0]);
return 1;
}
//创建服务器套接字
int service_sock = socket(AF_INET, SOCK_STREAM, 0);
if(service_sock < 0){
printf("create service_sock failed, errno : %d, error_string: %s\n", errno, strerror(errno));
return 2;
}
//初始化
struct sockaddr_in service_socket;
bzero(&service_socket, sizeof(service_socket));
service_socket.sin_family = AF_INET;
service_socket.sin_port = htons(atoi(argv[1]));
service_socket.sin_addr.s_addr = htonl(SERVICE_IP);
socklen_t service_socket_len = sizeof(service_socket);
//绑定端口号和IP地址
if((bind(service_sock, (struct sockaddr*)(&service_socket), service_socket_len)) < 0){
printf("bind failed, errno : %d, error_string: %s\n", errno, strerror(errno));
close(service_sock);
return 3;
}
//设置为监听状态
if((listen(service_sock, WAIT_QUEUE)) < 0){
printf("listen failed, errno : %d, error_string: %s\n", errno, strerror(errno));
close(service_sock);
return 4;
}
printf("service_socket bind listen success, wait accept...\n");
//进入循环等待链接
struct sockaddr_in client_socket;
while(1){
socklen_t client_socket_len = sizeof(client_socket);
//随时等待接入的客户端
int client_sock = accept(service_sock, (struct sockaddr*)(&client_socket), &client_socket_len);
if(client_sock < 0){
printf("accept failed, errno : %d, error_string: %s\n", errno, strerror(errno));
printf("wait next accept\n");
continue;
}
//获取接入客户端的IP地址和端口号并输出
/*INET_ADDRSTRLEN 16*/
char client_ip_buf[INET_ADDRSTRLEN];
char* ptr = inet_ntop(AF_INET, &client_socket.sin_addr, client_ip_buf, sizeof(client_ip_buf));
if(ptr == NULL){
printf("inet_ntop failed, errno : %d, error_string: %s\n", errno, strerror(errno));
continue;
}
printf("a new accept, client ip: %s, client port: %d\n", client_ip_buf, ntohs(client_socket.sin_port));
printf("waiting message from client\n");
//跟当前客户端进行阻塞式数据通信
while(1){
char buf[1024];
bzero(buf, sizeof(buf));
//等待客户端的数据
int num = recv(client_sock, buf, sizeof(buf), 0);
if(num == 0){
printf("client quit\n");
close(client_sock);
break;
}
if(num > 0){
printf("client# %s\n", buf);
printf("service# ");
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
//发送数据给客户端
int num1 = send(client_sock, buf, strlen(buf)+1, 0);
if(num1 < 0){
printf("send failed, errno : %d, error_string: %s\n", errno, strerror(errno));
break;
}
}
else{
printf("recv failed, errno : %d, error_string: %s\n", errno, strerror(errno));
break;
}
}
close(client_sock);
}
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// ./client service_ip service_port
int main(int argc, char* argv[]){
if(argc != 3){
printf("Usage: %s service_ip service_port\n", argv[0]);
return 1;
}
//创建套接字
int client_sock = socket(AF_INET, SOCK_STREAM, 0);
if(client_sock < 0){
printf("create client_sock failed, errno : %d, error_string: %s\n", errno, strerror(errno));
return 2;
}
//初始化套接字
struct sockaddr_in client_socket;
bzero(&client_socket, sizeof(client_socket));
client_socket.sin_family = AF_INET;
client_socket.sin_port = htons(atoi(argv[2]));
int num = inet_pton(AF_INET, argv[1], &client_socket.sin_addr);
if(num <= 0){
if(num == 0){
fprintf(stderr, "inet_pton not in presentation format");
}
else{
printf("inet_pton failed, errno : %d, error_string: %s\n", errno, strerror(errno));
}
return 3;
}
socklen_t client_socket_len= sizeof(client_socket);
//发起请求连接
if((connect(client_sock, (struct sockaddr*)(&client_socket), client_socket_len)) < 0){
printf("connect failed, errno : %d, error_string: %s\n", errno, strerror(errno));
close(client_sock);
return 3;
}
printf("connect success ...\n");
printf("please enter message\n");
//进入连接,开始循环发送数据
char buf[1024];
while(1){
bzero(buf, sizeof(buf));
printf("client# ");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
//发送数据
int num1 = send(client_sock, buf, strlen(buf)+1, 0);
if(num1 > 0){
bzero(buf, sizeof(buf));
int num2 = recv(client_sock, buf, sizeof(buf), 0);
if(num2 < 0){
//阻塞式等待数据接收
printf("recv failed, errno : %d, error_string: %s\n", errno, strerror(errno));
break;
}
printf("service# %s\n", buf);
}
else{
printf("send failed, errno : %d, error_string: %s\n", errno, strerror(errno));
break;
}
}
close(client_sock);
printf("client quit!!\n");
return 0;
}
Makefile
.phony:all
all:service client
flags=-Wall
service:service.c
gcc -o $@ $^ $(flags)
client:client.c
gcc -o $@ $^ $(flags)
.phony:clean
clean:
rm -f service client
实验截图:
这里是同一台虚拟机的不同终端下的测试,其实如果连接的是同一个网段,用不同的电脑或者用手机也是可以连接的,但是由于手机没有客户端代码,只能获取到信息,并不能进行通信。
手机通过浏览器,使用http协议进行访问,就相当于直接输入了http报头的信息。
单线程版本的缺点
单进程最大的问题不是效率,是每次服务器只能够处理一个客户端,这样是很不正常的。正常的是我们的服务器可以跟多个客户端一起连接,然后可以同时跟每个客户端一起通信。
单进程版本只能等一个client结束之后再处理下一client的请求。我们可以通过多线程和多进程的版本来解决这个问题。
参考资料
- UNIX环境高级编程