使用TCP套接字来开发一个模拟用户远程登陆的程序。
(一)服务器端程序的设计
- 服务器端的并发性
- 本程序使用多线程的方式来实现服务器对多个客户端连接请求的响应。
- 主程序创建套接字后将套接字和自主选定的端口进行绑定。
- 使套接字处于监听状态,调用accept函数等待来自客户端的连接请求。
- 每接受一个新的客户端连接请求,服务器端进程就创建一个子进程,在子进程中处理该连接请求。
- 服务器端进程继续等待来自其他客户端的连接请求。
- 数据格式
- TCP的数据传输方式为流式,数据没有固定的格式。
- 在应用程序中定义一定的数据格式,本程序采用回车符(’\n’)作为一次数据结束的标志。
- 用户信息
- 将用户信息保存在一个全局数组中。
- 服务器端接收到来自客户端的登陆用户名后,在该全局数组中查询是否存在该用户名。
- 若不存在,则回应字符’N’+结束标志’\n’
- 若存在,则回应字符’Y’+结束标志’\n’
- 等待客户端的密码数据
- 若密码不匹配,则回应字符’N’+结束标志’\n’
- 若密码匹配,则回应字符’Y’+结束标志’\n’,并发送一个欢迎登陆字符串给客户端
(二)客户端程序的设计
客户端的应用程序相对于服务器端要简单,客户端主程序创建套接字后调用connect函数连接到
服务器端的自主选定的端口,使用从connect函数返回的连接套接字与服务器端进行通信,交换数据。
(三)客户端流程解析
(1)参数输入方式
对于编译生成的可执行文件,我们使用命令行的形式输入服务器IP地址和端口号。这里利用main函数的参数argc(int)和argv(char **)来判断并读取输入的参数,其中用到了函数atoi将字符串转成整型变量。
- *argc(int)*表示有效输入的参数的个数。
- *argv(char **)*以字符串形式储存有效输入的参数。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
printf("The number of arguments is %d\n", argc);
int i;
for(i = 0; i < argc; ++i) {
printf("The %d-th argument entered is %s\n",i+1, argv[i]);
}
return 0;
}
- 关于输入格式的设计:***./Client.exe [-a] [Server Address] [-p] [Server Port]***
$ ./Client.exe -a 127.0.0.1 -p 4507
The number of arguments is 5
The 1-th argument entered is ./Client.exe
The 2-th argument entered is -a
The 3-th argument entered is 127.0.0.1
The 4-th argument entered is -p
The 5-th argument entered is 4507
所以首先要检查参数的个数是否满足5个,不满足就需要报错并给出提示来说明输入格式。
/* Check the number of arguments */
if(argc != 5) {
printf("Usage: [-a] [Server's IP address] [-p] [Server's Port]\n");
exit(1);
}
(2)初始化服务器端的地址结构
因为我们的程序是基于TCP的,这里设置struct sockaddr_in结构类型,协议族为AF_INET,即IPv4。之后从*argv(char **)*获取端口号和服务器地址。最后要检查输入参数的完整性。
/* Initialize the server-side address structure */
struct sockaddr_in Server_addr;
// memset() will also clear the padding field __pad to zero
memset(&Server_addr, 0, sizeof(struct sockaddr_in));
Server_addr.sin_family = AF_INET;
// Get server-side ports and addresses from command line arguments
int i;
for(i = 1; i < argc; ++i) { // Skip executable command
// First find the server's IP address
if(strcmp("-a", argv[i]) == 0) {
// One position is [-a], then the next position is [Server's IP address]
// inet_pton():convert IPv4 addresses from text to binary form
if(inet_aton(argv[i+1], &Server_addr.sin_addr) == 0) {
printf("Invalid Server IP Address\n");
exit(1);
}
continue;
}
// Then find the server port number
if(strcmp("-p", argv[i]) == 0) {
// One position is [-p], then the next position is [Server's Port]
int Server_port = atoi(argv[i+1]);
// Check the validity of the port number
// The port number ranges from 1 to 65535.
if(Server_port < 0 || 65535 < Server_port) {
printf("Server's port number is invalid.\n");
exit(1);
}
// host to internet short
Server_addr.sin_port = htons(Server_port);
continue;
}
}
// Check if an argument is missing
if(Server_addr.sin_port == 0 || Server_addr.sin_addr.s_addr == 0) {
printf("Usage: [-p] [Server_addr.sin_port] [-a] [Server_addr.sin_addr.s_addr]\n");
exit(1);
}
(3)创建套接字并向服务器端发送连接请求
/* Create a TCP socket */
int conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if(conn_fd < 0) {
ErrorHanding("socket", __LINE__);
}
/* Send a connection request to the server */
if(connect(conn_fd, (struct sockaddr *)&Server_addr, sizeof(struct sockaddr)) < 0) {
ErrorHanding("connect", __LINE__);
}
(4)连接成功后输入用户名和密码并判断是否登陆成功
-
GetUserInfo函数用来传递客户端输入的消息
-
InputUserInfo函数用来向服务器端发送客户端的输入数据并判断是否成功登陆
-
ErrorHanding函数是自定义的错误处理函数。
-
RecvData函数用来到自定义的缓存区中获取接收到的数据并返回接收数据的个数。
-
后面的两个函数的设计不在客户端程序考虑范围。
/* Enter username and password */
InputUserInfo(conn_fd, "Username");
InputUserInfo(conn_fd, "Password");
---
int GetUserInfo(char *buf, int len)
{
/* User input will be stored in buf */
// Null pointer cannot transmit user input
if(buf == NULL) {
return -1;
}
// Start saving user input data
int i = 0;
char msg;
while(((msg = getchar()) != '\n') && (msg != EOF) && (i < len-2)) {
buf[i++] = msg;
}
// You have to leave two spaces here for the end sign
buf[i++] = '\n';
buf[i] = '\0';
return 0;
}
void InputUserInfo(int conn_fd, const char *MsgString)
{
// The username entered by the client is sent through conn_fd
char input_buf[32];
char recei_buf[BUFSIZE];
int flag_UserInfo;
do {
// Here MsgString is output on the client
printf("%s:", MsgString);
if(GetUserInfo(input_buf, 32) < 0) {
printf("Get User data function sending Error.\n");
exit(1);
}
// Here it means that the user's input is normal
if(send(conn_fd, input_buf, strlen(input_buf), 0) < 0) {
ErrorHanding("send", __LINE__);
}
// The server must judge this after sending it
if(RecvData(conn_fd, recei_buf, sizeof(recei_buf)) < 0) {
printf("Too much data entered.\n");
exit(1);
}
if(recei_buf[0] == VALID_USERINFO) {
// The server indicates that the information entered is valid
flag_UserInfo = VALID_USERINFO;
} else {
printf("The %s entered is invalid, please try again.\n");
flag_UserInfo = INVALID_USERINFO;
}
} while(flag_UserInfo == INVALID_USERINFO);
}
(5)登陆成功返回服务器端的欢迎数据并关闭连接
// Successful login and output welcome message
char recv_buf[BUFSIZE];
int msgret; // The number of bytes read from the custom buffer
if((msgret = RecvData(conn_fd, recv_buf, sizeof(recv_buf))) < 0) {
printf("Data is too long\n");
exit(1);
}
// Output the message returned by the server after successful login
for(i = 0; i < msgret; ++i) {
printf("%c", recv_buf[i]);
}
printf("\n");
// Close connection after successfully receiving welcome message
close(conn_fd);
(四)服务器端流程解析
(1)创建一个套接字
// Create a socket
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd < 0) {
ErrorHanding("scoket", __LINE__);
}
(2)初始化服务器端地址结构
/* Initialize the server-side address structure */
struct sockaddr_in Server_addr;
memset(&Server_addr, 0, sizeof(struct sockaddr_in));
Server_addr.sin_family = AF_INET;
Server_addr.sin_port = htons(SERVER_PORT);
Server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
(3)设置套接字并将套接字绑定到对应的端口
/* Set up the socket so that it can reuse and bind ports
* If the socket that is already in the connected state
* is forcibly closed after calling close() without
* going through the process of TIME_WAIT, please set OptVal = 0
*/
int OptVal = 1;
if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&OptVal, sizeof(int)) < 0) {
ErrorHanding("setsockopt",__LINE__);
}
/* Bind socket to local port */
if(bind(sock_fd, (struct sockaddr *)&Server_addr, sizeof(struct sockaddr_in)) < 0) {
ErrorHanding("bind", __LINE__);
}
(4)使套接字成为监听套接字
/* Converting a socket to a listening socket */
if(listen(sock_fd, LISTEN_SIZE) < 0) {
ErrorHanding("listen", __LINE__);
}
(4)处理客户端发送的信息
struct sockaddr_in Client_addr;
while(1) {
/* Receive the client's connection request through accept()
* send and receive data using the returned connection socket
*/
socklen_t Client_len = sizeof(struct sockaddr_in);
int conn_fd = accept(sock_fd, (struct sockaddr *)&Client_addr, &Client_len);
if(conn_fd < 0) {
ErrorHanding("accept",__LINE__);
}
printf("Accept a New Client, IP: %s\n", inet_ntoa(Client_addr.sin_addr));
/* Create a new child process to handle the connection request just received.
* Closed connection by parent process.
* The return value of the child process is zero.
*/
pid_t pid;
if((pid = fork()) != 0) {
close(conn_fd);
continue;
}
// Child process starts here
int flag_recv = USERNAME;
int nameIdx;
while(1) {
char recv_buf[USERINFOSIZE];
int msglen;
if((msglen = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0) {
perror("recv");
exit(1);
}
// Turn end of received data into end of string
recv_buf[msglen-1] = '\0';
if(flag_recv == USERNAME) {
nameIdx = FindName(recv_buf);
switch(nameIdx) {
case -2:
exit(1);
case -1:
Send_data(conn_fd, "N\n");
break;
default:
Send_data(conn_fd, "Y\n");
flag_recv = PASSWORD;
break;
}
} else if(flag_recv == PASSWORD) {
if(strcmp(users[nameIdx].password, recv_buf) == 0) {
Send_data(conn_fd, "Y\n");
Send_data(conn_fd, "Welcom Login My TCP Server.\n");
printf("%s Login.\n",users[nameIdx].username);
break;
} else {
Send_data(conn_fd,"N\n");
}
}
}
close(conn_fd);
close(sock_fd);
exit(0);
}
(五)源代码示例
(1)Client.c
#include "CustomFun.h"
#define INVALID_USERINFO 'N' // User information is invalid
#define VALID_USERINFO 'Y' // User information is valid
const char *CLIENT_USAGE =
"Usage: [-a] [Server's IP address] [-p] [Server's Port]";
int GetUserInfo(char *buf, int len)
{
/* User input will be stored in buf */
// Null pointer cannot transmit user input
if(buf == NULL) {
return -1;
}
// Start saving user input data
int i = 0;
char msg;
while(((msg = getchar()) != '\n') && (msg != EOF) && (i < len-2)) {
buf[i++] = msg;
}
// You have to leave two spaces here for the end sign
buf[i++] = '\n';
buf[i] = '\0';
return 0;
}
void InputUserInfo(int conn_fd, const char *MsgString)
{
// The username entered by the client is sent through conn_fd
char input_buf[USERINFOSIZE];
char recv_buf[BUFSIZE];
int flag_UserInfo;
do {
// Here MsgString is output on the client
printf("%s:", MsgString);
if(GetUserInfo(input_buf, USERINFOSIZE) < 0) {
printf("function GetUserInfo() return Error.\n");
exit(1);
}
// Here it means that the user's input is normal
if(send(conn_fd, input_buf, strlen(input_buf), 0) < 0) {
ErrorHanding("send", __LINE__);
}
// The server must judge this after sending it
if(RecvMsg(conn_fd, recv_buf, sizeof(recv_buf)) < 0) {
printf("Too much data entered.\n");
exit(1);
}
if(recv_buf[0] == VALID_USERINFO) {
// The server indicates that the information entered is valid
flag_UserInfo = VALID_USERINFO;
} else {
printf("The [%s] entered is invalid, please try again.\n", MsgString);
flag_UserInfo = INVALID_USERINFO;
}
} while(flag_UserInfo == INVALID_USERINFO);
}
int main(int argc, char **argv)
{
/* Check the number of arguments */
if(argc != 5) {
printf("%s\n", CLIENT_USAGE);
exit(1);
}
/* Initialize the server-side address structure */
struct sockaddr_in Server_addr;
// memset() will also clear the padding field __pad to zero
memset(&Server_addr, 0, sizeof(struct sockaddr_in));
Server_addr.sin_family = AF_INET;
// Get server-side ports and addresses from command line arguments
int i;
for(i = 1; i < argc; ++i) { // Skip executable command
// First find the server's IP address
if(strcmp("-a", argv[i]) == 0) {
// One position is [-a], then the next position is [Server's IP address]
// inet_pton():convert IPv4 addresses from text to binary form
if(inet_aton(argv[i+1], &Server_addr.sin_addr) == 0) {
printf("Invalid Server IP Address\n");
exit(1);
}
continue;
}
// Then find the server port number
if(strcmp("-p", argv[i]) == 0) {
// One position is [-p], then the next position is [Server's Port]
int Server_port = atoi(argv[i+1]);
// Check the validity of the port number
// The port number ranges from 1 to 65535.
if(Server_port < 0 || 65535 < Server_port) {
printf("Server's port number is invalid.\n");
exit(1);
}
// host to internet short
Server_addr.sin_port = htons(Server_port);
}
}
// Check if an argument is missing
if(Server_addr.sin_port == 0 || Server_addr.sin_addr.s_addr == 0) {
printf("%s\n", CLIENT_USAGE);
exit(1);
}
/* Create a TCP socket */
int conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if(conn_fd < 0) {
ErrorHanding("socket", __LINE__);
}
/* Send a connection request to the server */
if(connect(conn_fd, (struct sockaddr *)&Server_addr, sizeof(struct sockaddr)) < 0) {
ErrorHanding("connect", __LINE__);
}
/* Enter username and password */
// This step will determine whether the server
// is legal according to the user's input.
InputUserInfo(conn_fd, "Username");
InputUserInfo(conn_fd, "Password");
// Successful login and output welcome message
char recv_buf[BUFSIZE];
int msglen; // The number of bytes read from the custom buffer
if((msglen = RecvMsg(conn_fd, recv_buf, sizeof(recv_buf))) < 0) {
printf("data is too long\n");
exit(1);
}
// Output the message returned by the server after successful login
for(i = 0; i < msglen; ++i) {
printf("%c", recv_buf[i]);
}
printf("\n");
// Close connection after successfully receiving welcome message
close(conn_fd);
return 0;
}
(2)Server.c
#include "CustomFun.h"
#define SERVER_PORT 8989 // Server-side port
#define LISTEN_SIZE 12 // Maximum connection request queue length
#define USERNAME 0 // Received username
#define PASSWORD 1 // Received password
struct UserInfo {
char username[USERINFOSIZE];
char password[USERINFOSIZE];
};
struct UserInfo users[4] = {
{"Unix", "System"},
{"Linux", "System"},
{"Windows", "System"},
// End of array with a string that only means one space
{" "," "}
};
int FindName(const char *ptrNameString)
{
/* Find if the user name exists.
* If there is an index, the user name will be returned.
*/
if(ptrNameString == NULL) {
printf("In Function FindName, NULL ptrNameString\n");
return -2;
}
int i;
for(i = 0; users[i].username[0] != ' '; ++i) {
if(strcmp(users[i].username, ptrNameString) == 0) {
return i;
}
}
return -1;
}
void Send_data(int conn_fd, const char *MsgString)
{
if(send(conn_fd, MsgString, strlen(MsgString), 0) < 0) {
ErrorHanding("send", __LINE__);
}
}
int main()
{
/* Create a socket */
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd < 0) {
ErrorHanding("scoket", __LINE__);
}
/* Set up the socket so that it can reuse and bind ports
* If the socket that is already in the connected state
* is forcibly closed after calling close() without
* going through the process of TIME_WAIT, please set OptVal = 0
*/
int OptVal = 1;
if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&OptVal, sizeof(int)) < 0) {
ErrorHanding("setsockopt",__LINE__);
}
/* Initialize the server-side address structure */
struct sockaddr_in Server_addr;
memset(&Server_addr, 0, sizeof(struct sockaddr_in));
Server_addr.sin_family = AF_INET;
Server_addr.sin_port = htons(SERVER_PORT);
Server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* Bind socket to local port */
if(bind(sock_fd, (struct sockaddr *)&Server_addr, sizeof(struct sockaddr_in)) < 0) {
ErrorHanding("bind", __LINE__);
}
/* Converting a socket to a listening socket */
if(listen(sock_fd, LISTEN_SIZE) < 0) {
ErrorHanding("listen", __LINE__);
}
struct sockaddr_in Client_addr;
while(1) {
/* Receive the client's connection request through accept()
* send and receive data using the returned connection socket
*/
socklen_t Client_len = sizeof(struct sockaddr_in);
int conn_fd = accept(sock_fd, (struct sockaddr *)&Client_addr, &Client_len);
if(conn_fd < 0) {
ErrorHanding("accept",__LINE__);
}
printf("Accept a New Client, IP: %s\n", inet_ntoa(Client_addr.sin_addr));
/* Create a new child process to handle the connection request just received.
* Closed connection by parent process.
* The return value of the child process is zero.
*/
pid_t pid;
if((pid = fork()) != 0) {
close(conn_fd);
continue;
}
// Child process starts here
int flag_recv = USERNAME;
int nameIdx;
while(1) {
char recv_buf[USERINFOSIZE];
int msglen;
if((msglen = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0) {
perror("recv");
exit(1);
}
// Turn end of received data into end of string
recv_buf[msglen-1] = '\0';
if(flag_recv == USERNAME) {
nameIdx = FindName(recv_buf);
switch(nameIdx) {
case -2:
exit(1);
case -1:
Send_data(conn_fd, "N\n");
break;
default:
Send_data(conn_fd, "Y\n");
flag_recv = PASSWORD;
break;
}
} else if(flag_recv == PASSWORD) {
if(strcmp(users[nameIdx].password, recv_buf) == 0) {
Send_data(conn_fd, "Y\n");
Send_data(conn_fd, "Welcom Login My TCP Server.\n");
printf("%s Login.\n",users[nameIdx].username);
break;
} else {
Send_data(conn_fd,"N\n");
}
}
}
close(conn_fd);
close(sock_fd);
exit(0);
}
}
(3)CustomFun.h
#ifndef __C_S_CustomFun_H
#define __C_S_CustomFun_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFSIZE 1024
#define USERINFOSIZE 32
void ErrorHanding(const char *ErrorFunName, int Position)
{
fprintf(stderr, "line:%d\n", Position);
perror(ErrorFunName);
exit(1);
}
int RecvMsg(int conn_fd, char *data_buf, int len)
{
static char recv_buf[BUFSIZE]; // Custom buffer
static char *pread = NULL; // Point to the next data read position
static int remain_len = 0; // Number of bytes remaining in the custom buffer
// If there is no data in the custom buffer
// then read data from the socket
if(remain_len <= 0) {
// Store data into a custom buffer and update the remaining bytes
if((remain_len = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0) {
ErrorHanding("recv", __LINE__);
} else if(remain_len == 0) {
// End symbol
return 0;
}
// The pointer needs to be updated in the normal storage area
pread = recv_buf;
}
// Read data from custom buffer once
int i;
for(i = 0; *pread != '\n'; ++i) {
if(len < i) {
// Can't read that much at once
return -1;
}
data_buf[i] = *pread++;
remain_len--;
}
// Skip end glyph
remain_len--;
pread++;
// Returns the number of bytes successfully read
return i;
}
#endif // __C_S_CustomFun_H
(六)示例
本应用程序只是给出了一个C/S模型的基本示例,程序具体实现还存在一些细节问题。
$ ./Client.exe -a 127.0.0.1 -p 8989
Username:Linux
Password:book
The [Password] entered is invalid, please try again.
Password:System
Welcom Login My TCP Server.
---
$ ./Server.exe
Accept a New Client, IP: 127.0.0.1
Linux Login.