文章目录
前言
本文介绍了socket通信的相关API,以及Redis 创始人 antirez 用纯 C 语言代码写了一个聊天服务器的最小编程示例,Smallchat。
一、socket的基本操作
socket是“open—write/read—close”模式的一种实现,下面以TCP为例,介绍几个基本的socket接口函数。
(1) socket()函数
int socket(int family, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
- domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
- protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
(2) bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
函数的三个参数分别为:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
- addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
}; /* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
- addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
网络字节序与主机字节序:
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
- Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
(3) listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
(4) accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
(5) read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。
1)write的返回值大于0,表示写了部分或者是全部的数据。
2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
(6) close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h>
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
二、smallchat
Smallchat 源代码已托管至 GitHub:https://github.com/antirez/smal
antirez 表示,编写这个示例是为了帮助他的前端开发朋友了解系统编程知识,比如单个进程执行多路复用、获取客户端状态,并在客户端拥有新数据后尝试快速访问此类状态,等等。
代码流程
1.initChat初始化全局变量,同时创建服务端监听fd保存到全局变量Chat->serversock中
2.开始while死循环
3.先初始化fd_set集合
4.将监听fd和客户端fd放入到fd_set集合中
5.调用系统函数select对fd_set集合进行事件监测,同时将监测到结果保存到fd_set中
6.最后在分别对监听fd和客户端fd在结果fd_set中是否有事件进行判断
7.分别进行对应的业务处理
smallchat-server.c
/* smallchat.c -- Read clients input, send to all the other connected clients. * * Copyright (c) 2023, Salvatore Sanfilippo <antirez at gmail dot com> * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the project name of nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include "chatlib.h"
/* ============================ Data structures ================================= * The minimal stuff we can afford to have. This example must be simple * even for people that don't know a lot of C. * =========================================================================== */
#define MAX_CLIENTS 1000 // This is actually the higher file descriptor.
#define SERVER_PORT 7711
/* This structure represents a connected client. There is very little * info about it: the socket descriptor and the nick name, if set, otherwise * the first byte of the nickname is set to 0 if not set. * The client can set its nickname with /nick <nickname> command. */
struct client {
int fd; // Client socket.
char *nick; // Nickname of the client.
};
/* This global structure encapsulates the global state of the chat. */
struct chatState {
int serversock; // Listening server socket.
int numclients; // Number of connected clients right now.
int maxclient; // The greatest 'clients' slot populated.
struct client *clients[MAX_CLIENTS]; // Clients are set in the corresponding
// slot of their socket descriptor.
};
struct chatState *Chat; // Initialized at startup.
/* ====================== Small chat core implementation ======================== * Here the idea is very simple: we accept new connections, read what clients * write us and fan-out (that is, send-to-all) the message to everybody * with the exception of the sender. And that is, of course, the most * simple chat system ever possible. * =========================================================================== */
/* Create a new client bound to 'fd'. This is called when a new client * connects. As a side effect updates the global Chat state. */
struct client *createClient(int fd) {
char nick[32]; // Used to create an initial nick for the user.
int nicklen = snprintf(nick,sizeof(nick),"user:%d",fd);
struct client *c = chatMalloc(sizeof(*c));
socketSetNonBlockNoDelay(fd); // Pretend this will not fail.
c->fd = fd;
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,nick,nicklen);
assert(Chat->clients[c->fd] == NULL); // This should be available.
Chat->clients[c->fd] = c;
/* We need to update the max client set if needed. */
if (c->fd > Chat->maxclient) Chat->maxclient = c->fd;
Chat->numclients++;
return c;
}
/* Free a client, associated resources, and unbind it from the global * state in Chat. */
void freeClient(struct client *c) {
free(c->nick);
close(c->fd);
Chat->clients[c->fd] = NULL;
Chat->numclients--;
if (Chat->maxclient == c->fd) {
/* Ooops, this was the max client set. Let's find what is * the new highest slot used. */
int j;
for (j = Chat->maxclient-1; j >= 0; j--) {
if (Chat->clients[j] != NULL) {
Chat->maxclient = j;
break;
}
}
if (j == -1) Chat->maxclient = -1; // We no longer have clients.
}
free(c);
}
/* Allocate and init the global stuff. */
void initChat(void) {
Chat = chatMalloc(sizeof(*Chat)); // 封装了一层malloc,申请内存失败时直接退出程序了
memset(Chat,0,sizeof(*Chat));
/* No clients at startup, of course. */
Chat->maxclient = -1;
Chat->numclients = 0;
/* Create our listening socket, bound to the given port. This * is where our clients will connect. */
Chat->serversock = createTCPServer(SERVER_PORT);
if (Chat->serversock == -1) {
perror("Creating listening socket");
exit(1);
}
}
/* Send the specified string to all connected clients but the one * having as socket descriptor 'excluded'. If you want to send something * to every client just set excluded to an impossible socket: -1. */
void sendMsgToAllClientsBut(int excluded, char *s, size_t len) {
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j] == NULL ||
Chat->clients[j]->fd == excluded) continue;
/* Important: we don't do ANY BUFFERING. We just use the kernel * socket buffers. If the content does not fit, we don't care. * This is needed in order to keep this program simple. */
write(Chat->clients[j]->fd,s,len);
}
}
/* The main() function implements the main chat logic: * 1. Accept new clients connections if any. * 2. Check if any client sent us some new message. * 3. Send the message to all the other clients. */
int main(void) {
initChat();
while(1) {
fd_set readfds;
struct timeval tv;
int retval;
FD_ZERO(&readfds); // 初始化文件描述符集
/* When we want to be notified by select() that there is * activity? If the listening socket has pending clients to accept * or if any other client wrote anything. */
FD_SET(Chat->serversock, &readfds); // 将文件描述符加入集合
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j]) FD_SET(j, &readfds);
}
/* Set a timeout for select(), see later why this may be useful * in the future (not now). */
tv.tv_sec = 1; // 1 sec timeout
tv.tv_usec = 0;
/* Select wants as first argument the maximum file descriptor * in use plus one. It can be either one of our clients or the * server socket itself. */
int maxfd = Chat->maxclient;
if (maxfd < Chat->serversock) maxfd = Chat->serversock;
retval = select(maxfd+1, &readfds, NULL, NULL, &tv);
if (retval == -1) {
perror("select() error");
exit(1);
} else if (retval) {
/* If the listening socket is "readable", it actually means * there are new clients connections pending to accept. */
if (FD_ISSET(Chat->serversock, &readfds)) {
int fd = acceptClient(Chat->serversock);
struct client *c = createClient(fd);
/* Send a welcome message. */
char *welcome_msg =
"Welcome to Simple Chat! "
"Use /nick <nick> to set your nick.\n";
write(c->fd,welcome_msg,strlen(welcome_msg));
printf("Connected client fd=%d\n", fd);
}
/* Here for each connected client, check if there are pending * data the client sent us. */
char readbuf[256];
for (int j = 0; j <= Chat->maxclient; j++) {
if (Chat->clients[j] == NULL) continue;
if (FD_ISSET(j, &readfds)) {
/* Here we just hope that there is a well formed * message waiting for us. But it is entirely possible * that we read just half a message. In a normal program * that is not designed to be that simple, we should try * to buffer reads until the end-of-the-line is reached. */
int nread = read(j,readbuf,sizeof(readbuf)-1);
if (nread <= 0) {
/* Error or short read means that the socket * was closed. */
printf("Disconnected client fd=%d, nick=%s\n",
j, Chat->clients[j]->nick);
freeClient(Chat->clients[j]);
} else {
/* The client sent us a message. We need to * relay this message to all the other clients * in the chat. */
struct client *c = Chat->clients[j];
readbuf[nread] = 0;
/* If the user message starts with "/", we * process it as a client command. So far * only the /nick <newnick> command is implemented. */
if (readbuf[0] == '/') {
/* Remove any trailing newline. */
char *p;
p = strchr(readbuf,'\r'); if (p) *p = 0;
p = strchr(readbuf,'\n'); if (p) *p = 0;
/* Check for an argument of the command, after * the space. */
char *arg = strchr(readbuf,' ');
if (arg) {
*arg = 0; /* Terminate command name. */
arg++; /* Argument is 1 byte after the space. */
}
if (!strcmp(readbuf,"/nick") && arg) {
free(c->nick);
int nicklen = strlen(arg);
c->nick = chatMalloc(nicklen+1);
memcpy(c->nick,arg,nicklen+1);
} else {
/* Unsupported command. Send an error. */
char *errmsg = "Unsupported command\n";
write(c->fd,errmsg,strlen(errmsg));
}
} else {
/* Create a message to send everybody (and show * on the server console) in the form: * nick> some message. */
char msg[256];
int msglen = snprintf(msg, sizeof(msg),
"%s> %s", c->nick, readbuf);
/* snprintf() return value may be larger than * sizeof(msg) in case there is no room for the * whole output. */
if (msglen >= (int)sizeof(msg))
msglen = sizeof(msg)-1;
printf("%s",msg);
/* Send it to all the other clients. */
sendMsgToAllClientsBut(j,msg,msglen);
}
}
}
}
} else {
/* Timeout occurred. We don't do anything right now, but in * general this section can be used to wakeup periodically * even if there is no clients activity. */
}
}
return 0;
}
smallchat-client.c
/* smallchat-client.c -- Client program for smallchat-server. * * Copyright (c) 2023, Salvatore Sanfilippo <antirez at gmail dot com> * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the project name of nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#include "chatlib.h"
/* ============================================================================ * Low level terminal handling. * ========================================================================== */
void disableRawModeAtExit(void);
/* Raw mode: 1960 magic shit. */
int setRawMode(int fd, int enable) {
/* We have a bit of global state (but local in scope) here. * This is needed to correctly set/undo raw mode. */
static struct termios orig_termios; // Save original terminal status here.
static int atexit_registered = 0; // Avoid registering atexit() many times.
static int rawmode_is_set = 0; // True if raw mode was enabled.
struct termios raw;
/* If enable is zero, we just have to disable raw mode if it is * currently set. */
if (enable == 0) {
/* Don't even check the return value as it's too late. */
if (rawmode_is_set && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)
rawmode_is_set = 0;
return 0;
}
/* Enable raw mode. */
if (!isatty(fd)) goto fatal;
if (!atexit_registered) {
atexit(disableRawModeAtExit);
atexit_registered = 1;
}
if (tcgetattr(fd,&orig_termios) == -1) goto fatal;
raw = orig_termios; /* modify the original mode */
/* input modes: no break, no CR to NL, no parity check, no strip char, * no start/stop output control. */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - do nothing. We want post processing enabled so that * \n will be automatically translated to \r\n. */
// raw.c_oflag &= ...
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - choing off, canonical off, no extended functions, * but take signal chars (^Z,^C) enabled. */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
/* control chars - set return condition: min number of bytes and timer. * We want read to return every single byte, without timeout. */
raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
/* put terminal in raw mode after flushing */
if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
rawmode_is_set = 1;
return 0;
fatal:
errno = ENOTTY;
return -1;
}
/* At exit we'll try to fix the terminal to the initial conditions. */
void disableRawModeAtExit(void) {
setRawMode(STDIN_FILENO,0);
}
/* ============================================================================ * Mininal line editing. * ========================================================================== */
void terminalCleanCurrentLine(void) {
write(fileno(stdout),"\e[2K",4);
}
void terminalCursorAtLineStart(void) {
write(fileno(stdout),"\r",1);
}
#define IB_MAX 128
struct InputBuffer {
char buf[IB_MAX]; // Buffer holding the data.
int len; // Current length.
};
/* inputBuffer*() return values: */
#define IB_ERR 0 // Sorry, unable to comply.
#define IB_OK 1 // Ok, got the new char, did the operation, ...
#define IB_GOTLINE 2 // Hey, now there is a well formed line to read.
/* Append the specified character to the buffer. */
int inputBufferAppend(struct InputBuffer *ib, int c) {
if (ib->len >= IB_MAX) return IB_ERR; // No room.
ib->buf[ib->len] = c;
ib->len++;
return IB_OK;
}
void inputBufferHide(struct InputBuffer *ib);
void inputBufferShow(struct InputBuffer *ib);
/* Process every new keystroke arriving from the keyboard. As a side effect * the input buffer state is modified in order to reflect the current line * the user is typing, so that reading the input buffer 'buf' for 'len' * bytes will contain it. */
int inputBufferFeedChar(struct InputBuffer *ib, int c) {
switch(c) {
case '\n':
break; // Ignored. We handle \r instead.
case '\r':
return IB_GOTLINE;
case 127: // Backspace.
if (ib->len > 0) {
ib->len--;
inputBufferHide(ib);
inputBufferShow(ib);
}
break;
default:
if (inputBufferAppend(ib,c) == IB_OK)
write(fileno(stdout),ib->buf+ib->len-1,1);
break;
}
return IB_OK;
}
/* Hide the line the user is typing. */
void inputBufferHide(struct InputBuffer *ib) {
(void)ib; // Not used var, but is conceptually part of the API.
terminalCleanCurrentLine();
terminalCursorAtLineStart();
}
/* Show again the current line. Usually called after InputBufferHide(). */
void inputBufferShow(struct InputBuffer *ib) {
write(fileno(stdout),ib->buf,ib->len);
}
/* Reset the buffer to be empty. */
void inputBufferClear(struct InputBuffer *ib) {
ib->len = 0;
inputBufferHide(ib);
}
/* ============================================================================= * Main program logic, finally :) * ========================================================================== */
int main(int argc, char **argv) {
if (argc != 3) {
printf("Usage: %s <host> <port>\n", argv[0]);
exit(1);
}
/* Create a TCP connection with the server. */
int s = TCPConnect(argv[1],atoi(argv[2]),0);
if (s == -1) {
perror("Connecting to server");
exit(1);
}
/* Put the terminal in raw mode: this way we will receive every * single key stroke as soon as the user types it. No buffering * nor translation of escape sequences of any kind. */
setRawMode(fileno(stdin),1); // 获取标准输入流 stdin 的文件描述符
/* Wait for the standard input or the server socket to * have some data. */
fd_set readfds;
int stdin_fd = fileno(stdin);
struct InputBuffer ib;
inputBufferClear(&ib);
while(1) {
FD_ZERO(&readfds);
FD_SET(s, &readfds);
FD_SET(stdin_fd, &readfds);
int maxfd = s > stdin_fd ? s : stdin_fd;
int num_events = select(maxfd+1, &readfds, NULL, NULL, NULL);
if (num_events == -1) {
perror("select() error");
exit(1);
} else if (num_events) {
char buf[128]; /* Generic buffer for both code paths. */
if (FD_ISSET(s, &readfds)) {
/* Data from the server? */
ssize_t count = read(s,buf,sizeof(buf));
if (count <= 0) {
printf("Connection lost\n");
exit(1);
}
inputBufferHide(&ib);
write(fileno(stdout),buf,count);
inputBufferShow(&ib);
} else if (FD_ISSET(stdin_fd, &readfds)) {
/* Data from the user typing on the terminal? */
ssize_t count = read(stdin_fd,buf,sizeof(buf));
for (int j = 0; j < count; j++) {
int res = inputBufferFeedChar(&ib,buf[j]);
switch(res) {
case IB_GOTLINE:
inputBufferAppend(&ib,'\n');
inputBufferHide(&ib);
write(fileno(stdout),"you> ", 5);
write(fileno(stdout),ib.buf,ib.len);
write(s,ib.buf,ib.len);
inputBufferClear(&ib);
break;
case IB_OK:
break;
}
}
}
}
}
close(s);
return 0;
}
chatlib.c
#define _POSIX_C_SOURCE 200112L
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ======================== Low level networking stuff ========================== * Here you will find basic socket stuff that should be part of * a decent standard C library, but you know... there are other * crazy goals for the future of C: like to make the whole language an * Undefined Behavior. * =========================================================================== */
/* Set the specified socket in non-blocking mode, with no delay flag. */
int socketSetNonBlockNoDelay(int fd) {
int flags, yes = 1;
/* Set the socket nonblocking. * Note that fcntl(2) for F_GETFL and F_SETFL can't be * interrupted by a signal. */
if ((flags = fcntl(fd, F_GETFL)) == -1) return -1; // 获取套接字当前的文件状态标志
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) return -1; // 将套接字设置为非阻塞模式
/* This is best-effort. No need to check for errors. */
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));
return 0;
}
/* Create a TCP socket listening to 'port' ready to accept connections. */
int createTCPServer(int port) {
int s, yes = 1;
struct sockaddr_in sa;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) return -1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); // Best effort.
memset(&sa,0,sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY表示服务器将监听所有网络接口上的连接请求
if (bind(s,(struct sockaddr*)&sa,sizeof(sa)) == -1 ||
listen(s, 511) == -1)
{
close(s);
return -1;
}
return s;
}
/* Create a TCP socket and connect it to the specified address. * On success the socket descriptor is returned, otherwise -1. * * If 'nonblock' is non-zero, the socket is put in nonblocking state * and the connect() attempt will not block as well, but the socket * may not be immediately ready for writing. */
int TCPConnect(char *addr, int port, int nonblock) {
int s, retval = -1;
struct addrinfo hints, *servinfo, *p;
char portstr[6]; /* Max 16 bit number string length. */
snprintf(portstr,sizeof(portstr),"%d",port);
memset(&hints,0,sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(addr,portstr,&hints,&servinfo) != 0) return -1;
for (p = servinfo; p != NULL; p = p->ai_next) {
/* Try to create the socket and to connect it. * If we fail in the socket() call, or on connect(), we retry with * the next entry in servinfo. */
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
continue;
/* Put in non blocking state if needed. */
if (nonblock && socketSetNonBlockNoDelay(s) == -1) {
close(s);
break;
}
/* Try to connect. */
if (connect(s,p->ai_addr,p->ai_addrlen) == -1) {
/* If the socket is non-blocking, it is ok for connect() to * return an EINPROGRESS error here. */
if (errno == EINPROGRESS && nonblock) return s;
/* Otherwise it's an error. */
close(s);
break;
}
/* If we ended an iteration of the for loop without errors, we * have a connected socket. Let's return to the caller. */
retval = s;
break;
}
freeaddrinfo(servinfo);
return retval; /* Will be -1 if no connection succeded. */
}
/* If the listening socket signaled there is a new connection ready to * be accepted, we accept(2) it and return -1 on error or the new client * socket on success. */
int acceptClient(int server_socket) {
int s;
while(1) {
struct sockaddr_in sa;
socklen_t slen = sizeof(sa);
s = accept(server_socket,(struct sockaddr*)&sa,&slen);
if (s == -1) {
if (errno == EINTR)
continue; /* Try again. */
else
return -1;
}
break;
}
return s;
}
/* We also define an allocator that always crashes on out of memory: you * will discover that in most programs designed to run for a long time, that * are not libraries, trying to recover from out of memory is often futile * and at the same time makes the whole program terrible. */
void *chatMalloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL) {
perror("Out of memory");
exit(1);
}
return ptr;
}
/* Also aborting realloc(). */
void *chatRealloc(void *ptr, size_t size) {
ptr = realloc(ptr,size);
if (ptr == NULL) {
perror("Out of memory");
exit(1);
}
return ptr;
}
文章评论