所谓client/server简单来说就是客户端服务器模型,简称C/S模型,一个简单的CS模型所用到的只是一些简单的网络方面的知识,下面我以一个面向连接的CS实例来解释面向连接的主要过程:
首先我们想看一张图,来了解一下服务器端和客户端的链接过程:
首先是服务器端,服务器首先要创建套接字,然后将其绑定到本地端口,之后将其转换为链接套接字,之后就时阻塞等待客户端的连接了。
在这里解释一下端口和IP地址之间的关系:我们都知道IP地址就如同家庭的地址和门牌号,能够找到某人的家,这里就如同找到某台主机是一样的,但是家里的人不止一个,就如同一台主机同时运行的应用程序有好多一样,要将消息准发到特定的应用程序就需要用到端口号,端口号就是该应用程序消息的接口。一般情况下,某个应用程序的端口是特定的,例如:浏览器的端口是80一样。
那么套接字又是干什么的呢?套接字中保存有客户端课服务器端的IP地址和连接套接字,有了连接套接字就能将信息发出了。
客户端同时也需要建立一个连接套接字,在客户端将IP地址和端口等信息初始化完成之后,就需要将客户端与服务器连接,此时服务器处于阻塞状态等待客户端的连接,当客户端连接成功之后,就会产生一个连接套接字,消息通过连接套接字发送和接收。
上图读取数据和发送数据就是通过send函数和recv函数来实现的。
在服务器和客户端之间一般是发送一些特定的数据包,例如整个结构体数据,我们可以利用memcpy函数将结构体保存在一个字符串数组当中,然后等到在客户端接收之后,再将其还原为一个结构体。
下面是实例的代码:
客户端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "my_recv.h"
#define INVALID_USERINFO 'n' //用户信息无效
#define VALID_USERINFO 'y' //用户信息有效
int get_userinfo(char *buf, int len) //获取用户输入存入buf,长度len,以'\n'结束
{
int i;
int c;
if(buf == NULL)
{
return -1;
}
i = 0;
while(((c = getchar()) != '\n') && (c != EOF) && (i < len - 2))
{
buf[i++] = c;
}
buf[i++] = '\n';
buf[i++] = '\0';
return 0;
}
void input_userinfo(int conn_fd, const char *string) //输入用户名,然后通过fd发送出去
{
char input_buf[32];
char recv_buf[BUFSIZE];
int flag_userinfo;
do
{ //输入用户信息知道正确为止
printf("%s:", string);
if(get_userinfo(input_buf, 32) < 0)
{
printf("error return from get_userinfo\n");
exit(1);
}
if(send(conn_fd, input_buf,strlen(input_buf), 0) < 0)
{
my_err("send", __LINE__);
}
if(my_recv(conn_fd, recv_buf, sizeof(recv_buf)) < 0)
{
printf("data is too long\n");
exit(1);
}
if(recv_buf[0] == VALID_USERINFO)
{
flag_userinfo = VALID_USERINFO;
}
else
{
printf("%s error, input again,", string);
flag_userinfo = INVALID_USERINFO;
}
}while(flag_userinfo == INVALID_USERINFO);
}
int main(int argc, char ** argv)
{
int i;
int ret;
int conn_fd;
int serv_port;
struct sockaddr_in serv_addr;
char recv_buf[BUFSIZE];
if(argc != 5) //检查参数个数
{
printf("usage : [-p] [serv_port] [-a] [serv_address]\n");
exit(1);
}
memset(&serv_addr, 0, sizeof(struct sockaddr_in)); //初始化服务器端地址结构
serv_addr.sin_family = AF_INET;
for(i = 1; i < argc; i++) //从命令行获取服务器的端口与地址
{
if(strcmp("-p", argv[i]) == 0)
{
serv_port = atoi(argv[i+1]);
if(serv_port < 0 || serv_port > 65535)
{
printf("invalid serv_addr.sin_port\n");
exit(1);
}
else
{
serv_addr.sin_port = htons(serv_port);
}
continue;
}
if(strcmp("-a", argv[i]) == 0)
{
if(inet_aton(argv[i+1], &serv_addr.sin_addr) == 0)
{
printf("invalid server ip address\n");
exit(1);
}
continue;
}
}
if(serv_addr.sin_port == 0 || serv_addr.sin_addr.s_addr == 0) //检测是否少输入了某项参数
{
printf("usage: [-p] [serv_addr.sin_port] [-a] [serv_address]\n");
exit(1);
}
conn_fd = socket(AF_INET, SOCK_STREAM, 0); //创建一个tcp套接字
if(conn_fd < 0){
my_err("socket", __LINE__);
}
if(connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) < 0){ //向服务器端发送连接请求
my_err("connect", __LINE__);
}
input_userinfo(conn_fd, "username"); //输入用户名和密码
input_userinfo(conn_fd, "password");
if((ret = my_recv(conn_fd, recv_buf, sizeof(recv_buf))) < 0){ //读取欢迎信息并打印
printf("data is too long\n");
exit(1);
}
for(i = 0; i < ret; i++){
printf("%c", recv_buf[i]);
}
printf("\n");
close(conn_fd);
return 0;
}
服务器端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include "my_recv.h"
#include<stdlib.h>
#define SERV_PORT 4507 //服务器的端口
#define LISTENQ 12 //连接请求队列的最大长度
#define INVALID_USERINFO 'n' //用户信息无效
#define VALID_USERINFO 'y' //用户信息有效
#define USERNAME 0 //接受到的是用户名
#define PASSWORD 1 //接受到的是密码
struct userinfo //保存用户名和密码的结构体
{
char username[32];
char password[32];
};
struct userinfo users[] =
{
{"linux", "unix"},
{"4507", "4508"},
{"clh", "clh"},
{"xl", "xl"},
{" ", " "} //以只含一个空格的字符串作为数组的结束标志
};
int find_name(const char * name)
{
int i;
if(name == NULL)
{
printf("in find_name, NULL pointer");
return -2;
}
for(i = 0; users[i].username[0] != ' '; i++)
{
if(strcmp(users[i].username, name) == 0)
{
return i;
}
}
return -1;
}
void send_data(int conn_fd, const char *string)
{
if(send(conn_fd, string, strlen(string), 0) < 0)
{
my_err("send", __LINE__); //my_err函数在my_recv.h中声明
}
}
int main()
{
int sock_fd, conn_fd;
int optval;
int flag_recv = USERNAME;
int ret;
int name_num;
pid_t pid;
socklen_t cli_len;
struct sockaddr_in cli_addr, serv_addr;
char recv_buf[128];
sock_fd = socket(AF_INET, SOCK_STREAM, 0); //创建一个TCP套接字
if(sock_fd < 0)
{
my_err("socket", __LINE__);
}
optval = 1; //设置该套接字使之可以重新绑定端口
if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(int)) < 0)
{
my_err("setsocketopt", __LINE__);
}
memset(&serv_addr, 0, sizeof(struct sockaddr_in)); //初始化服务器端地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0) //将套接字绑定到本地端口
{
my_err("bind", __LINE__);
}
if(listen(sock_fd, LISTENQ) < 0) //将套接字转化为监听套接字
{
my_err("listen", __LINE__);
}
cli_len = sizeof(struct sockaddr_in);
while(1)
{
conn_fd = accept(sock_fd, (struct sockaddr *)&cli_addr, &cli_len);
if(conn_fd < 0)
{
my_err("accept", __LINE__);
}
printf("accept a new client, ip: %s\n", inet_ntoa(cli_addr.sin_addr));
if((pid = fork()) == 0) //创建子进程处理刚刚接收的连接请求
{
while(1) //子进程
{
if((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
{
perror("recv");
exit(1);
}
recv_buf[ret - 1] = '\0'; //将数据结束标志'\n'替换成字符串结束标志
if(flag_recv == USERNAME) //接收到的是用户名
{
name_num = find_name(recv_buf);
switch (name_num)
{
case -1:
send_data(conn_fd, "n\n");
break;
case -2:
exit(1);
break;
default:
send_data(conn_fd, "y\n");
flag_recv = PASSWORD;
break;
}
}
else if(flag_recv == PASSWORD) //接收到的是密码
{
if (strcmp(users[name_num].password, recv_buf) == 0)
{
send_data(conn_fd, "y\n");
send_data(conn_fd, "welcome login my tcp server\n");
printf("%s login \n", users[name_num].username);
break;
}
else
{
send_data(conn_fd, "n\n");
}
}
}
close(sock_fd);
close(sock_fd);
exit(0); //结束子进程
}
else
{
close(conn_fd); //父进程关闭刚刚接收的连接请求,执行accept等待其他连接请求
}
}
return 0;
}
my_recv.h
#ifndef _MY_RECV_H
#define _MY_RECV_H
#define BUFSIZE 1024
void my_err(const char * err_string, int line);
int my_recv(int conn_fd, char * data_buf, int len);
#endif
这里自定义了一个读取数据的函数,实际就是将套接字缓冲区的数据拷贝到自定义缓冲区(以”\n”为结束标志),然后再按格式将数据读出(效果和上面所说的memcpy函数是一样的)。
在该例中,当收到一个新的客户端的连接请求之后,服务器端就会创建一个新的进程来处理客户端的请求。关于进程创建可以参考进程控制。
在该客户端,首先创建了一个TCP套接字,然后调用函数connect请求与服务器端连接,建立连接之后,通过连接套接字首先发送用户名,然后等待服务器确认,若用户存在,则发送密码,若密码正确,则返回欢迎页面。
下面是可执行文件的链接地址:github
执行本程序时,首先在某一终端运行服务器端程序,然后在另外几个终端运行客户端。在客户端执行时输入如下数据:
./client -a 127.0.0.1 -p 4507
服务器中默认存在用户,用户名和密码分别是:
{“linux”, “unix”}, {“4507”, “4508”}, {“clh”, “clh”}, {“xl”, “xl”}。