套接字
C程序有那个数据流读写字节,如文件,标准输入输出等,如果想写一个与网络通信的程序,就需要一种新的数据流-套接字(socket)
#include
...
int listener_d = socket(PF_INET,SOCK_STREAM,0);
//listener_d似乎套接字的描述符->互联网套接字
//0是协议号,默认填0就可以。
、、、
if(listener_d == -1)
error("无法打开套接字");
BLAB:服务器连接网络四部曲
B代表绑定端口(Bind)
L代表监听(Listen)
A代表接受连接(Accept)
B代表开始通信(Begin)
使用套接字于客户端程序通信前,服务器需要经历四个阶段:绑定(Bind)
、监听(Listen)
、接受(Accept)
、和开始(Begin)
。
1、绑定端口
为了防止不同对话发送混乱,没醒服务必须使用不同的端口(port)
。
#include
//为了创建互联网地址,需要包含这些头文件
...
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port =(in_port_t)htons(30000);
name.sin_addr.s_addr = htonl(INADDR_ANY);
//这些代码创建一个表示"互联网30000端口"套接字名
int c = bind(listener_d,(struct sockaddr *)&name,sizeof(name));
if(c == -1)
error("无法绑定端口");
2、监听
如果创建的服务器出了名,可能会有很多客户端同时连接,想让客户端排队等待连接。
if(listen(listener_d,10) == -1)
error("无法监听");
//列队长度为10,最对可以有10个客户端挺尸尝试连接服务器,它们不会立即等到响应,但可以排队等待,而第11个客户端回被服务器告知服务器太忙。
3、接受连接,
绑定完端口,设置完监听,唯一可以做的就是等待,accept()系统调用会一直等待,知道有客户端连接服务器时,它才会返回第二个套接字描述符,然后就可以用它通信了。
struct sockaddr_storage client_addr;
unsigned int address_size = sizeof(client_addr);
int connect_d = accept(listener_d,(struct sockaddr *)&client_addr,&address_size);
if(cinnect_d == -1)
error("无法打开副套接字");
服务器将用新的连接描述符connect_d
··· ··· 开始通信
套接字不是传统意义上的数据流
套接字是双向的,即可以用作输入,也可以有那个做输出,也就是说,他要用其他函数和他通信
如果想想套接字输出数据,要用send()
,而不是fprintf()
.
char *msg = "Internet Konck-Knock Protocol Server\r\nVersion 1.0\r\nKonck!Knock!\r\n> ";
if(send(connect_d,meg,strlen(msg),0) == -1)
error("send")
绑定端口有延时
某个端口绑定套接字,接下来的30秒内,操作系统不予许任何程序再绑定它,包括商议绑定这个端口的程序。
int reuse = 1;
//需要一个整形量来保存选项。
//设置为1,表示重新使用端口
if(setsockopt(listener_d,SOL_SOCKET,SO_REUSEADDR,(char*)&reuse,sizeof(int)) == -1)
error("无法设置套接字的\"重新使用端口\"选项");
以上代码,让套接字重新使用已绑定过的端口。这样在第二次绑定端口时就不会发生错误了。
advice_server.c
#include
#include
#include
#include
#include
#include //close()
#include
void error(char *msg)
{
fprintf(stderr, "%s: %s\n",msg,strerror(errno) );
exit(1);
}
int main(int argc, char *argv[])
{
char *advice[]={
"Take smaller bites\r\n",
"Go for the tight jeans. No they do NOT make you look fat.\r\n",
"One word:inappropriate\r\n",
"Just for today ,be honest.Tell your boss what you *really* think\r\n",
"You might want to rethink that haircut\r\n"
};
int listener_d = socket(PF_INET,SOCK_STREAM,0);
if(listener_d == -1)
error("无法打开套接字");
//listener_d似乎套接字的描述符->互联网套接字
//0是协议号,默认填0就可以。
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port =(in_port_t)htons(30000);
name.sin_addr.s_addr = htonl(INADDR_ANY);
//这些代码创建一个表示"互联网30000端口"套接字名
int reuse = 1;
//需要一个整形量来保存选项。
//设置为1,表示重新使用端口
if(setsockopt(listener_d,SOL_SOCKET,SO_REUSEADDR,(char*)&reuse,sizeof(int)) == -1)
error("无法设置套接字的\"重新使用端口\"选项");
if(bind(listener_d,(struct sockaddr *)&name,sizeof(name)) == -1)
error("无法绑定端口");
if(listen(listener_d,10) == -1)
error("无法监听");
puts("Wanting for connection");
while(1)
{
struct sockaddr_storage client_addr;
unsigned int address_size = sizeof(client_addr);
int connect_d = accept(listener_d,(struct sockaddr *)&client_addr,&address_size);
if(connect_d == -1)
error("无法打开副套接字");
char *msg =advice[rand() % 5];
//rand 是一个随机函数
send(connect_d,msg,strlen(msg),0);
}
return 0;
}
从客户端读取数据
套接字用send()写数据,用recv()数据:
#include
...
<读了几个字节> = recv(<描述符>,<缓冲区>,<要读几个字节>,0);
如果用户在客户端输入一行文本,然后按下回车,recv()
函数就会把文本保存在一个这样的字符数组中:
字符串不能以
\0
结尾。当用户在telent输入文本时,字符串以
\r\n
结尾。recv()
返回字符个数,如果发生错误就返回-1
,如果客户端关闭了就返回0
.-
recv()
调用不一定能一次接收到所有字符。最后一点很重要,他意味着可能需要多次调用recv()。
简单的recv()封装函数:
int read_in(int socket, char *buf,int len)
{
char *s = buf;
int slen = len;
int c = recv(socket,s,slen,0);
while((c >0)&&(s[c-1] != '\n')){
s += c;
slen -= c;
c = recv(socket,s,slen,0);
}
if(c <0)
return c;
eles if(c == 0)
buf[0] = '\0';
else
s[c-1] = '\0';
return len - slen;
}
小测试:
代码:
#include
#include
#include
#include
#include
#include //close()
#include
#include
void error(char *msg)
{
fprintf(stderr, "%s: %s\n",msg,strerror(errno) );
exit(1);
}
/*
注册处理器函数
参数: 信号编号,处理器函数指针
*/
int catch_signal(int sig,void (*handler)(int))
{
struct sigaction action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
return sigaction(sig,&action,NULL);
}
int open_listener_socket()
{
int s = socket(PF_INET,SOCK_STREAM,0);
if(s == -1)
error("无法打开套接字");
return s;
}
void bind_to_port(int socket,int port)
{
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port =(in_port_t)htons(30000);
name.sin_addr.s_addr = htonl(INADDR_ANY);
//这些代码创建一个表示"互联网30000端口"套接字名
int reuse = 1;
//需要一个整形量来保存选项。
//设置为1,表示重新使用端口
if(setsockopt(socket,SOL_SOCKET,SO_REUSEADDR,(char*)&reuse,sizeof(int)) == -1)
error("无法设置套接字的\"重新使用端口\"选项");
if(bind(socket,(struct sockaddr *)&name,sizeof(name)) == -1)
error("无法绑定端口");
}
int read_in(int socket, char *buf,int len)
{
char *s = buf;
int slen = len;
int c = recv(socket,s,slen,0);
while((c >0)&&(s[c-1] != '\n')){
s += c;
slen -= c;
c = recv(socket,s,slen,0);
}
if(c <0)
return c;
else if(c == 0)
buf[0] = '\0';
else
s[c-1] = '\0';
return len - slen;
}
/*
向客户端发送字符串
*/
int say(int socket,char *s)
{
int result = send(socket,s,strlen(s),0);
if(result == -1)
fprintf(stderr, "%s:%s\n", "和客户端通信时发生错误",strerror(errno));
return result;
}
int listener_d;//它将保存服务器的主监听套接字
/*
如果有人在服务器运行期间按住了Ctrl+C,这个函数就会在程序结束前关闭套接字
*/
void handle_shutdown(int sig)
{
if(listener_d)
close(listener_d);
fprintf(stderr,"Bye!\n");
exit (0);
}
int main(int argc, char const *argv[])
{
if(catch_signal(SIGINT,handle_shutdown) == -1)
error("Can't set the interrupt handler");
listener_d = open_listener_socket();
bind_to_port(listener_d,30000);
if(listen(listener_d,10) == -1)
error("can't listen");
struct sockaddr_storage client_addr;
unsigned int address_size = sizeof(client_addr);
puts("Waiting for connection");
char buf[255];
while(1)
{
int connect_d = accept(listener_d,(struct sockaddr *)&client_addr,&address_size);
if(connect_d == -1)
error("Can't open secondary socket");
if(say(connect_d,"Jnternet Knock-Knock Protocol server \r\nVersion 1.0\r\nKnkck! Knock!\r\n") != -1)
read_in(connect_d,buf,sizeof(buf));
if(strncasecmp("Who's there?",buf,12))
say(connect_d,"You should say 'Who's there?'!");
else{
if(say(connect_d,"Oscar\r\n") != -1){
read_in(connect_d,buf,sizeof(buf));
if(strncasecmp("Oscar who?",buf,10))
say(connect_d,"You should say 'Oscar Who?'!\r\n");
else
say(connect_d,"Oscar silly question,you get a silly answer\r\n");
}
}
close(connect_d);
}
return 0;
}
运行:
#./ikkp_server
#telnet 127.0.0.1 30000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Jnternet Knock-Knock Protocol server
Version 1.0
Knkck! Knock!
#Who's there?
Oscar
#Oscar who?
Oscar silly question,you get a silly answer
Connection closed by foreign host.
#telnet 127.0.0.1 30000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Jnternet Knock-Knock Protocol server
Version 1.0
Knkck! Knock!
#Come in
You should say 'Who's there?'!Connection closed by foreign host.
为每一个客户端fork()一个子进程
修改代码:添加fork()
while(1)
{
int connect_d = accept(listener_d,(struct sockaddr *)&client_addr,&address_size);
if(connect_d == -1)
error("Can't open secondary socket");
if(!fork()) //创建子进程,如果fork返回0,就说明你在子进程中
{
close(listener_d);//在子进程需要关闭住监听套接字,子进程只用connect_d和客户端通信。
if(say(connect_d,"Jnternet Knock-Knock Protocol server \r\nVersion 1.0\r\nKnkck! Knock!\r\n") != -1)
read_in(connect_d,buf,sizeof(buf));
if(strncasecmp("Who's there?",buf,12))
say(connect_d,"You should say 'Who's there?'!");
else{
if(say(connect_d,"Oscar\r\n") != -1){
read_in(connect_d,buf,sizeof(buf));
if(strncasecmp("Oscar who?",buf,10))
say(connect_d,"You should say 'Oscar Who?'!\r\n");
else
say(connect_d,"Oscar silly question,you get a silly answer\r\n");
}
}
close(connect_d);
exit(0);//退出子进程,防止子进程进入服务器的主循环。
}
close(connect_d);
}