@SovietPower
2022-05-18T17:26:00.000000Z
字数 11690
阅读 1314
CN
服务器
gcc server.c -o server
./server
客户端
gcc client.c -o client
./client server_host client_host client_port
参数分别表示:服务器地址、客户端使用的地址、端口。后两项用于建立服务器客户端的全双工通信。
服务器端口默认为7700。
运行后,服务器即可接收客户端消息,并在连接时提示当前连接的客户端的socket列表。
在服务器端任意时刻,输入client_socket info
,可发送消息info
给指定的、socket为client_socket
的已连接客户端。
客户端连续输入两个空行,则与该客户端连接终止。
就双方都判断一下,是否连续收到两个空行。
不确定这里服务器是否需要输出这两个空行,如果不需要,再判断一下即可。
这个用多线程是错的,不能保证线程是依次完成输出或单个线程的输出是原子操作。
容易想到,只需要:当前需要发送的客户端就发,不发的加跳过,依次枚举一遍,然后检查一下是否有新的客户端连接,这样死循环。
但有个问题是,接受请求要用accept
,accept
是阻塞的。
将accept
变为非阻塞
man accept
,可以看到:
ERRORS
EAGAIN or EWOULDBLOCK
The socket is marked nonblocking and no connections are present to be ac‐
cepted. POSIX.1-2001 and POSIX.1-2008 allow either error to be returned for
this case, and do not require these constants to have the same value, so a
portable application should check for both possibilities.
所以accept
是可以变为非阻塞的,只需要将socket
设为nonblocking
。
再man socket
,可以看到:
The socket has the indicated type, which specifies the communication semantics. Currently defined types are:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based
byte streams. An out-of-band data transmission mecha‐
nism may be supported.
...
Some socket types may not be implemented by all protocol families.
Since Linux 2.6.27, the type argument serves a second purpose: in addi‐
tion to specifying a socket type, it may include the bitwise OR of any
of the following values, to modify the behavior of socket():
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the open file
description (see open(2)) referred to by the new file
descriptor. Using this flag saves extra calls to fc‐
ntl(2) to achieve the same result.
所以我们将SOCK_NONBLOCK
和SOCK_STREAM
一样传入socket()
就可以了,也就是
将socket(PF_INET, SOCK_STREAM, 0)
改为socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)
,即将accept
变为了非阻塞。
然后要区分真正的错误和非阻塞的错误。
在errno-base.h
和errno.h
(用errno.h
一层层找,在比较深的位置),有:
// errno-base.h
#define EAGAIN 11 /* Try again */
// errno.h
#define EWOULDBLOCK EAGAIN /* Operation would block */
(perror
函数就会根据errno,输出这里面的注释)
当然改完后自己输出一下errno也可得到:
errno: 11
simplex-talk: accept: Resource temporarily unavailable
只需要在accept
出错时,判断errno类型是不是EAGAIN
(或EWOULDBLOCK
)即可。
if ((new_s = accept(s, (struct sockaddr *)&sin, &len)) < 0)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("server: accept");
exit(1);
}
}
else
保存新的socket
将recv
变为非阻塞
写完之后可以发现,当有多个进程同时发消息时,是轮流发的,在发送之前,不能成功发送任何消息,即使很久都不发送。发送后,这期间发送的所有消息中的最后一条才会展示出来(以前的没有缓存所以就没了?)。
因为recv
也是阻塞的。
man recv
可以发现,同样可设置socket为SOCK_NONBLOCK
,不过是发送方也就是要设置客户端的socket。
改完后发现,客户端有错误:Operation now in progress,即:
#define EINPROGRESS 115 /* Operation now in progress */
man connect
,可以发现:
EINPROGRESS
The socket is nonblocking and the connection cannot be completed
immediately. (UNIX domain sockets failed with EAGAIN instead.)
It is possible to select(2) or poll(2) for completion by select‐
ing the socket for writing. After select(2) indicates writabil‐
ity, use getsockopt(2) to read the SO_ERROR option at level
SOL_SOCKET to determine whether connect() completed successfully
(SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the
usual error codes listed here, explaining the reason for the
failure).
大概是变为非阻塞后,connection
需一段时间才能完成,要use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET
检查连接是否完成。
然后再man getsockopt
,发现太多了,就先忽略这个错误,假设可以很快完成。
然后再run,发现recv
还是阻塞。
所以在socket这设置,对recv
似乎是没有影响的。
man recv
还有,The flags argument
是可以设置MSG_DONTWAIT
的,表示不阻塞。
然后通过判断recv
的错误码是不是EAGAIN
(或EWOULDBLOCK
),就可以非阻塞接受多个客户端的信息了。
int len = recv(s_now, b, MAX_LINE, MSG_DONTWAIT);
if (len > 0)
{
printf("from client:%d len:%d\n", s_now, len);
fputs(b, stdout);
}
else if (len == -1 && errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("server recv");
exit(1);
}
就是指定对应的参数?
在client
建立连接时,用个while(1)
,直到成功连接时才退出,否则一直读入。
当时没这么做,代码没写。
设置客户端和服务器的buf
足够大即可?
以及要注意,服务器对每个连接的客户端,都需有一个独立的buf。
用文件作为客户端输入的话,服务器读不到两个换行,不能主动终止,应该不是什么问题。(其实用户这样也没直接输入两个换行?)
将fgets
变为非阻塞
首先要实现非阻塞的读入。
man fgets
中只有,unlocking
的信息在unlocked_stdio
中。
https://www.cnblogs.com/khler/archive/2010/12/20/1911701.html
open
时,是可以直接设置NONBLOCK
flag的。
对于stdin,可以通过fcntl
改变其flag。
然后就可以正常用read
从stdin读了。同样是判断errno是不是EAGAIN
。
void InitStdin()
{
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) < 0)
{
perror("fcntl");
exit(1);
}
}
int len = read(STDIN_FILENO, callback, MAX_LINE);
if (len > 0)
printf("callback: %s len:%d\n", callback, len);
else if (len < 0)
{
if (errno != EAGAIN)
{
perror("read stdin");
exit(1);
}
}
这样服务器的非阻塞读入和输出,就做完了。再复制给客户端一个。
然后要实现双工,需要混合一下服务器和客户端代码,也就是双方都需要listen, connect, send, recv
。
流程:
连接建立:server、client执行listen,client执行connect,server执行accept,client发送自己的地址和端口用于server构造地址,然后执行accept,server执行connect。
然后双方正常自由发送信息。
断开:当client发送两个换行符时,双方都执行close。结束。
listen、accept的socket,和connect、send的socket,不能使用同一个。
connect的一次只能connect一个。注意connect
用完一定要close
。
具体内容太麻烦了,不写文字了。
客户端
// by SovietPower
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h> // 定义了STDIN_FILENO
#define SERVER_PORT 7700
#define MAX_LINE 30000 // 30KB
extern int errno;
char buf[MAX_LINE];
char callback[MAX_LINE];
void InitStdin()
{
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) < 0)
{
perror("fcntl");
exit(1);
}
}
int main(int argc, char * argv[])
{
InitStdin();
char *host, *chost, *cport;
int client, server, connection;
if (argc==4)
host = argv[1], chost = argv[2], cport = argv[3];
else
{
fprintf(stderr, "usage: client server_host client_host client_port\n");
exit(1);
}
// todo (没做)输入错误的host反馈,但就是整个while直到读到正确的结果
// GetAddr();
/* translate host name into peer’s IP address */
struct hostent *hp = gethostbyname(host);
if (!hp)
{
fprintf(stderr, "client: unknown host: %s.\n", host);
exit(1);
}
// 主动连接到server
if ((connection = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
perror("client: socket");
exit(1);
}
// 监听server的请求
struct sockaddr_in cin;
bzero((char *)&cin, sizeof(cin));
cin.sin_family = AF_INET;
cin.sin_addr.s_addr = INADDR_ANY;
cin.sin_port = htons(atoi(cport));
if ((client = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
perror("client: socket");
exit(1);
}
// 监听socket必须绑定sockaddr
if ((bind(client, (struct sockaddr *)&cin, sizeof(cin))) < 0)
{
perror("client: bind");
exit(1);
}
// server sockaddr
struct sockaddr_in sin;
bzero((char *)&sin, sizeof(sin));
sin.sin_family = AF_INET;
bcopy(hp->h_addr, (char *)&sin.sin_addr, hp->h_length);
sin.sin_port = htons(SERVER_PORT);
// 连接到server
if (connect(connection, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
// if (errno != EINPROGRESS)
{
close(connection);
perror("client: connect");
exit(1);
}
}
listen(client, 2);
fprintf(stderr, "client: %d listening.\n",client);
// client发送自己的地址和端口用于server构造地址
int lenH = strlen(chost), lenP = strlen(cport);
for (int i=0; i<lenH; ++i) callback[i] = chost[i];
for (int i=0; i<lenP; ++i) callback[i+lenH+1] = cport[i];
callback[lenH]=' ', callback[lenH+lenP+1]='\0';
fprintf(stderr, "Sending connection information: %s\n", callback);
if (send(connection, callback, lenH+lenP+2, 0) < 0) // 注意发送长度要多1!发送'\0'
{
perror("client: first send");
exit(1);
}
// 接收server请求
int addr_len;
fprintf(stderr, "Wating to be connected.");
if ((server = accept(client, (struct sockaddr *)&cin, &addr_len)) < 0)
{
// 这里就不阻塞了
// if (errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("client: accept");
exit(1);
}
}
fprintf(stderr, "Build with server %d\n", server);
int enter = 0, len;
memset(callback, 0, sizeof callback); // clear!
while(1)
{
// 服务器发送的信息
len = recv(server, buf, MAX_LINE, MSG_DONTWAIT);
if (len > 0)
{
fprintf(stderr, "from server:%d len:%d\n", server, len);
fputs(buf, stdout);
}
else if (len == -1 && errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("client: recv");
exit(1);
}
// 发送给服务器
len = read(STDIN_FILENO, callback, MAX_LINE);
if (len > 0)
{
callback[len] = '\0'; // !
// fprintf(stderr, "callback: %s len:%d\n", callback, len);
len = send(connection, callback, len+1, 0);
if (len==2 && (callback[0]=='\n'||callback[0]=='\r'))
{
if (!enter) enter = 1;
else break;
}
else enter = 0;
}
else if (len < 0)
{
if (errno != EAGAIN)
{
perror("client: read stdin");
exit(1);
}
}
}
close(connection);
printf("Close!\n");
return 0;
}
服务器
// by SovietPower
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h> // 定义了STDIN_FILENO
#define SERVER_PORT 7700
#define MAX_PENDING 5
#define MAX_LINE 30000 // 30KB
extern int errno;
char buf[MAX_PENDING][MAX_LINE];
char callback[MAX_LINE];
int server, clients[MAX_PENDING], enter[MAX_PENDING];
int connections[MAX_PENDING];
int FindFree(int *clients)
{
for(int i=0; i<MAX_PENDING; ++i)
if (clients[i]==-1)
return i;
return -2;
}
void InitStdin()
{
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) < 0)
{
perror("fcntl");
exit(1);
}
}
int InitConnection(int i)
{
if ((connections[i] = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
perror("server: connection socket");
return -1;
}
return 0;
}
int main()
{
InitStdin();
// 主动连接到client
for (int i=0; i<MAX_PENDING; ++i)
clients[i] = -1, enter[i] = 0;
// 监听client的请求
struct sockaddr_in sin;
bzero((char *)&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(SERVER_PORT);
if ((server = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) < 0)
{
perror("server: socket");
exit(1);
}
// 监听socket必须绑定sockaddr
if ((bind(server, (struct sockaddr *)&sin, sizeof(sin))) < 0)
{
perror("server: bind");
exit(1);
}
listen(server, MAX_PENDING);
fprintf(stderr, "server: %d listening.\n",server);
int client, addr_len;
while(1)
{
if ((client = accept(server, (struct sockaddr *)&sin, &addr_len)) < 0)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("server: accept");
exit(1);
}
}
else
{
int pos = FindFree(clients);
if (pos == -2)
{
fprintf(stderr, "No enough space for the coming request.\n");
exit(1);
}
else
{
// 与用户建立连接
// 接受用户的host和port(不阻塞)
char tmp[20];
int len = recv(client, tmp, 20, 0), sep=-1;
for (int i=0; i<20; ++i)
if (tmp[i] == ' ')
{
tmp[sep=i] = '\0';
break;
}
if (sep == -1)
{
fprintf(stderr, "Request from client %d has an invalid argument. Connection failed.\n", client);
goto LISTENING;
// exit(1);
}
char *host = tmp, *port = tmp+sep+1;
fprintf(stderr, "Connecting with client %d: %s %s.\n", client, host, port);
/* translate host name into peer’s IP address */
struct hostent *hp = gethostbyname(host);
if (!hp)
{
fprintf(stderr, "server: Request from client %d has an unknown host: %s.\n", client, host);
goto LISTENING;
// exit(1);
}
// client sockaddr
struct sockaddr_in cin;
bzero((char *)&cin, sizeof(cin));
cin.sin_family = AF_INET;
bcopy(hp->h_addr, (char *)&cin.sin_addr, hp->h_length);
cin.sin_port = htons(atoi(port));
sleep(1);
// 连接到client
if (InitConnection(pos) < 0)
{
perror("server: init connection");
goto LISTENING;
}
if (connect(connections[pos], (struct sockaddr *)&cin, sizeof(cin)) < 0)
{
close(connections[pos]);
perror("server: connect");
goto LISTENING;
}
fprintf(stderr, "Connecting with %d on %d.\n", client, connections[pos]);
// 分配用户
enter[pos] = 0;
clients[pos] = client;
fprintf(stderr, "Build with %d\n", client);
fprintf(stderr, "当前socket列表:");
for(int i=0; i<MAX_PENDING; ++i)
if (~clients[i])
fprintf(stderr, "%d ",clients[i]);
fprintf(stderr, "\n");
}
}
LISTENING:
// 多用户发送
for (int i=0; i<MAX_PENDING; ++i)
{
int c_now = clients[i];
if (c_now == -1) continue;
char *b = buf[i];
int len = recv(c_now, b, MAX_LINE, MSG_DONTWAIT);
if (len > 0)
{
fprintf(stderr, "from client:%d len:%d actually:%ld\n", c_now, len, strlen(b));
fputs(b, stdout);
fflush(stdout);
if (len==2 && (b[0]=='\n'||b[0]=='\r'))
{
if (!enter[i]) enter[i] = 1;
else
{
fprintf(stderr, "Close with %d\n", c_now);
close(connections[i]);
close(c_now);
clients[i] = -1;
// connections[i] = -1; // 描述符不需要清空,好像必须要重新用socket创建?
}
}
else if (len>=2 && (b[0]=='\n'||b[0]=='\r') && (b[1]=='\n'||b[1]=='\r'))
{
fprintf(stderr, "Close with %d\n", c_now);
close(connections[i]);
close(c_now);
clients[i] = -1;
}
else enter[i] = 0;
}
else if (len == -1 && errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("server recv");
exit(1);
}
}
// 发送给指定用户信息
int len = read(STDIN_FILENO, callback, MAX_LINE);
if (len > 0)
{
callback[len] = '\0';
// fprintf(stderr, "callback: %s len:%d\n", callback, len);
int sep = -1;
for (int i=0; i<len; ++i)
if (callback[i] == ' ')
{
sep = i;
break;
}
if (sep == -1)
{
fprintf(stderr, "Invalid argument. Usage: client_socket info\n");
goto DONE;
}
int pos = -1, val = atoi(callback);
for (int i=0; i<MAX_PENDING; ++i)
if (clients[i] == val) pos = i;
if (pos == -1)
{
fprintf(stderr, "Invalid argument. Sending failed with %d.\n", val);
goto DONE;
}
fprintf(stderr, "Sent info to client %d\n", val);
send(connections[pos], callback+sep+1, len+1-sep-1, 0);
}
else if (len < 0)
{
if (errno != EAGAIN)
{
perror("server: read stdin");
exit(1);
}
}
DONE:
;
}
return 0;
}