第13章 基于数据报的编程:编写许可证服务器
1.许可证管理
2.流和数据报的比较
3.数据报编程
(1)接收数据报
/************************************************************************
* dgrecv.c - datagram receiver
* usage: dgrecv portnum
* action: listens at the specfied port and reports messages
*/
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<stdlib.h>
#define oops(m,x) { perror(m);exit(x);}
int make_dgram_server_socket(int);
int get_internet_address(char *, int, int *, struct sockaddr_in *);
void say_who_called(struct sockaddr_in *);
int main(int ac, char *av[])
{
int port; /* use this port */
int sock; /* for this socket */
char buf[BUFSIZ]; /* to receive data here */
size_t msglen; /* store its length here */
struct sockaddr_in saddr; /* put sender's address here */
socklen_t saddrlen; /* and its length here */
if ( ac == 1 || (port = atoi(av[1])) <= 0 ){
fprintf(stderr,"usage: dgrecv portnumber\n");
exit(1);
}
/* get a socket and assign it a port number */
if( (sock = make_dgram_server_socket(port)) == -1 )
oops("cannot make socket",2);
/* receive messaages on that socket */
saddrlen = sizeof(saddr);
while( (msglen = recvfrom(sock,buf,BUFSIZ,0,
(struct sockaddr *) &saddr,&saddrlen))>0 ) {
buf[msglen] = '\0';
printf("dgrecv: got a message: %s\n", buf);
say_who_called(&saddr);
}
return 0;
}
void say_who_called(struct sockaddr_in *addrp)
{
char host[BUFSIZ];
int port;
get_internet_address(host,BUFSIZ,&port,addrp);
printf(" from: %s:%d\n", host, port);
}
(2)发送数据报
/*********************************************************************
* dgsend.c - datagram sender
* usage: dgsend hostname portnum "message"
* action: sends message to hostname:portnum
*/
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
#define oops(m,x) { perror(m);exit(x);}
int make_dgram_client_socket();
int make_internet_address(char *,int, struct sockaddr_in *);
int main(int ac, char *av[])
{
int sock; /* use this socket to send */
char *msg; /* send this messag */
struct sockaddr_in saddr; /* put sender's address here */
if ( ac != 4 ){
fprintf(stderr,"usage: dgsend host port 'message'\n");
exit(1);
}
msg = av[3];
/* get a datagram socket */
if( (sock = make_dgram_client_socket()) == -1 )
oops("cannot make socket",2);
/* combine hostname and portnumber of destination into an address */
if ( make_internet_address(av[1], atoi(av[2]), &saddr) == -1 )
oops("make addr",4);
/* send a string through the socket to that address */
if ( sendto(sock, msg, strlen(msg), 0,
(struct sockaddr *)&saddr,sizeof(saddr)) == -1)
oops("sendto failed", 3);
return 0;
}
(3)辅助函数
/***************************************************************
* dgram.c
* support functions for datagram based programs
*/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<string.h>
#define HOSTLEN 256
int make_internet_address();
int make_dgram_server_socket(int portnum)
{
struct sockaddr_in saddr; /* build our address here */
char hostname[HOSTLEN]; /* address */
int sock_id; /* the socket */
sock_id = socket(PF_INET, SOCK_DGRAM, 0); /* get a socket */
if ( sock_id == -1 ) return -1;
/** build address and bind it to socket **/
gethostname(hostname, HOSTLEN); /* where am I ? */
make_internet_address(hostname, portnum, &saddr);
if ( bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0 )
return -1;
return sock_id;
}
int make_dgram_client_socket()
{
return socket(PF_INET, SOCK_DGRAM, 0);
}
int make_internet_address(char *hostname, int port, struct sockaddr_in *addrp)
/*
* constructor for an Internet socket address, uses hostname and port
* (host,port) -> *addrp
*/
{
struct hostent *hp;
bzero((void *)addrp, sizeof(struct sockaddr_in));
hp = gethostbyname(hostname);
if ( hp == NULL ) return -1;
bcopy((void *)hp->h_addr, (void *)&addrp->sin_addr, hp->h_length);
addrp->sin_port = htons(port);
addrp->sin_family = AF_INET;
return 0;
}
int get_internet_address(char *host, int len, int *portp, struct sockaddr_in *addrp)
/*
* extracts host and port from an internet socket address
* *addrp -> (host,port)
*/
{
strncpy(host, inet_ntoa(addrp->sin_addr), len );
*portp = ntohs(addrp->sin_port);
return 0;
}
(4)运行结果
启动服务器使得它监听端口4444,客户发送字符串到端口4444,客户socket拥有主机地址和端口号,内核随机给它分配了一个端口号56923。
(5)程序函数
sendto和recvfrom函数
地址转换函数:inet_aton & inet_ntoa & inet_addr和inet_pton & inet_ntop
在Unix网络编程中,我们常用到地址转换函数,它将ASCII字符串(如"206.62.226.33")与网络字节序的二进制值(这个值保存在套接口地址结构中)间进行地址的转换。
1、inet_aton、inet_addr和inet_ntoa在点分十进制数串(例如"206.62.226.33")与它的32位网络字节序二进制值间转换IPv4地址。
2、两个较新的函数:inet_pton和inet_ntop对IPv4和IPv6地址都能进行处理。
#include<arpa/inet.h>
/* 返回1:串有效,返回0:串出错 */
int inet_aton(const char *strptr, struct in_addr *addrptr);
/* 若成功,返回32位二进制的网络字节序地址;若出错,返回INADDR_NONE */
in_addr_t inet_addr(const char *strptr);
/* 返回指向点分十进制数串的指针 */
char* inet_ntoa(struct in_addr inaddr);
#include<arpa/inet.h>
/* 若函数成功,则返回1;若输入不是有效的格式,则函数返回0;若处理失败,函数返回-1 */
int inet_pton(int family, const char *strptr, void *addrptr);
/* 若函数处理成功,返回指向结果的指针;若函数处理失败,返回NULL */
const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
4.许可证服务器版本1.0
(1)客户端版本1
/****************************************************************************
* lclnt1.c
* License server client version 1
* link with lclnt_funcs1.o dgram.o
*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void do_regular_work();
int main(int ac, char *av[])
{
setup();
if (get_ticket() != 0 )
exit(0);
do_regular_work();
release_ticket();
shut_down();
}
/****************************************************************************
* do_regular_work the main work of the application goes here
*/
void do_regular_work()
{
printf("SuperSleep version 1.0 Running - Licensed Software\n");
sleep(10); /* our patented sleep algorithm */
}
/***************************************************************************
* lclnt_funcs1.c: functions for the client of the license server
*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<string.h>
#include<stdlib.h>
/*
* Important variables used throughout
*/
static int pid = -1; /* Our PID */
static int sd = -1; /* Our communications socket */
static struct sockaddr serv_addr; /* Server address */
static socklen_t serv_alen; /* length of address */
static char ticket_buf[128]; /* Buffer to hold our ticket */
static have_ticket = 0; /* Set when we have a ticket */
#define MSGLEN 128 /* Size of our datagrams */
#define SERVER_PORTNUM 2020 /* Our server's port number */
#define HOSTLEN 512
#define oops(p) { perror(p); exit(1) ; }
char *do_transaction(char *msg);
/****************************************************************************
* narrate: print messages to stderr for debugging and demo purposes
* IN msg1, msg2 : strings to print along with pid and title
* RET nothing, dies on error
*/
void narrate(char *msg1, char *msg2)
{
fprintf(stderr,"CLIENT [%d]: %s %s\n", pid, msg1, msg2);
}
void syserr(char *msg1)
{
char buf[MSGLEN];
sprintf(buf,"CLIENT [%d]: %s", pid, msg1);
perror(buf);
}
/*
* setup: get pid, socket, and address of license server
* IN no args
* RET nothing, dies on error
* notes: assumes server is on same host as client
*/
void setup()
{
char hostname[BUFSIZ];
pid = getpid(); /* for ticks and msgs */
sd = make_dgram_client_socket(); /* to talk to server */
if ( sd == -1 )
oops("Cannot create socket");
gethostname(hostname, HOSTLEN); /* server on same host */
make_internet_address(hostname, SERVER_PORTNUM, &serv_addr);
serv_alen = sizeof(serv_addr);
}
void shut_down()
{
close(sd);
}
/****************************************************************************
* get_ticket
* get a ticket from the license server
* Results: 0 for success, -1 for failure
*/
int get_ticket()
{
char *response;
char buf[MSGLEN];
if(have_ticket) /* don't be greedy */
return(0);
sprintf(buf, "HELO %d", pid); /* compose request */
if ( (response = do_transaction(buf)) == NULL )
return(-1);
/* parse the response and see if we got a ticket.
* on success, the message is: TICK ticket-string
* on failure, the message is: FAIL failure-msg
*/
if ( strncmp(response, "TICK", 4) == 0 ){
strcpy(ticket_buf, response + 5); /* grab ticket-id */
have_ticket = 1; /* set this flag */
narrate("got ticket", ticket_buf);
return(0);
}
if ( strncmp(response,"FAIL",4) == 0)
narrate("Could not get ticket",response);
else
narrate("Unknown message:", response);
return(-1);
} /* get_ticket */
/****************************************************************************
* release_ticket
* Give a ticket back to the server
* Results: 0 for success, -1 for failure
*/
int release_ticket()
{
char buf[MSGLEN];
char *response;
if(!have_ticket) /* don't have a ticket */
return(0); /* nothing to release */
sprintf(buf, "GBYE %s", ticket_buf); /* compose message */
if ( (response = do_transaction(buf)) == NULL )
return(-1);
/* examine response
* success: THNX info-string
* failure: FAIL error-string
*/
if ( strncmp(response, "THNX", 4) == 0 ){
narrate("released ticket OK","");
return 0;
}
if ( strncmp(response, "FAIL", 4) == 0)
narrate("release failed", response+5);
else
narrate("Unknown message:", response);
return(-1);
} /* release_ticket */
/****************************************************************************
* do_transaction
* Send a request to the server and get a response back
* IN msg_p message to send
* Results: pointer to message string, or NULL for error
* NOTE: pointer returned is to static storage
* overwritten by each successive call.
* note: for extra security, compare retaddr to serv_addr (why?)
*/
char *do_transaction(char *msg)
{
static char buf[MSGLEN];
struct sockaddr retaddr;
socklen_t addrlen;
int ret;
ret = sendto(sd, msg, strlen(msg), 0, &serv_addr, serv_alen);
if ( ret == -1 ){
syserr("sendto");
return(NULL);
}
/* Get the response back */
ret = recvfrom(sd, buf, MSGLEN, 0, &retaddr, &addrlen);
if ( ret == -1 ){
syserr("recvfrom");
return(NULL);
}
/* Now return the message itself */
return(buf);
} /* do_transaction */
(2)服务器版本1
/****************************************************************************
* lsrv1.c
* License server server program version 1
*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/errno.h>
#define MSGLEN 128 /* Size of our datagrams */
int main(int ac, char *av[])
{
struct sockaddr_in client_addr;
socklen_t addrlen;
char buf[MSGLEN];
int ret;
int sock;
sock = setup();
while(1) {
addrlen = sizeof(client_addr);
ret = recvfrom(sock,buf,MSGLEN,0,
(struct sockaddr*)&client_addr,&addrlen);
if ( ret != -1 ){
buf[ret] = '\0';
narrate("GOT:", buf, &client_addr);
handle_request(buf,&client_addr,addrlen);
}
else if ( errno != EINTR )
perror("recvfrom");
}
}
/****************************************************************************
* lsrv_funcs1.c
* functions for the license server
*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<signal.h>
#include<sys/errno.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#define SERVER_PORTNUM 2020 /* Our server's port number */
#define MSGLEN 128 /* Size of our datagrams */
#define TICKET_AVAIL 0 /* Slot is available for use */
#define MAXUSERS 3 /* Only 3 users for us */
#define oops(x) { perror(x); exit(-1); }
/****************************************************************************
* Important variables
*/
int ticket_array[MAXUSERS]; /* Our ticket array */
int sd = -1; /* Our socket */
int num_tickets_out = 0; /* Number of tickets outstanding */
char *do_hello();
char *do_goodbye();
void narrate(char *msg1, char *msg2, struct sockaddr_in *clientp);
void free_all_tickets();
/****************************************************************************
* setup() - initialize license server
*/
int setup()
{
sd = make_dgram_server_socket(SERVER_PORTNUM);
if ( sd == -1 )
oops("make socket");
free_all_tickets();
return sd;
}
void free_all_tickets()
{
int i;
for(i=0; i<MAXUSERS; i++)
ticket_array[i] = TICKET_AVAIL;
}
/****************************************************************************
* shut_down() - close down license server
*/
void shut_down()
{
close(sd);
}
/****************************************************************************
* handle_request(request, clientaddr, addrlen)
* branch on code in request
*/
void handle_request(char *req,struct sockaddr_in *client, socklen_t addlen)
{
char *response;
int ret;
/* act and compose a response */
if ( strncmp(req, "HELO", 4) == 0 )
response = do_hello(req);
else if ( strncmp(req, "GBYE", 4) == 0 )
response = do_goodbye(req);
else
response = "FAIL invalid request";
/* send the response to the client */
narrate("SAID:", response, client);
ret = sendto(sd, response, strlen(response),0,
(struct sockaddr *) client, addlen);
if ( ret == -1 )
perror("SERVER sendto failed");
}
/****************************************************************************
* do_hello
* Give out a ticket if any are available
* IN msg_p message received from client
* Results: ptr to response
* NOTE: return is in static buffer overwritten by each call
*/
char *do_hello(char *msg_p)
{
int x;
static char replybuf[MSGLEN];
if(num_tickets_out >= MAXUSERS)
return("FAIL no tickets available");
/* else find a free ticket and give it to client */
for(x = 0; x<MAXUSERS && ticket_array[x] != TICKET_AVAIL; x++)
;
/* A sanity check - should never happen */
if(x == MAXUSERS) {
narrate("database corrupt","",NULL);
return("FAIL database corrupt");
}
/* Found a free ticket. Record "name" of user (pid) in array.
* generate ticket of form: pid.slot
*/
ticket_array[x] = atoi(msg_p + 5); /* get pid in msg */
sprintf(replybuf, "TICK %d.%d", ticket_array[x], x);
num_tickets_out++;
return(replybuf);
} /* do_hello */
/****************************************************************************
* do_goodbye
* Take back ticket client is returning
* IN msg_p message received from client
* Results: ptr to response
*/
char *do_goodbye(char *msg_p)
{
int pid, slot; /* components of ticket */
/* The user's giving us back a ticket. First we need to get
* the ticket out of the message, which looks like:
*
* GBYE pid.slot
*/
if((sscanf((msg_p + 5), "%d.%d", &pid, &slot) != 2) ||
(ticket_array[slot] != pid)) {
narrate("Bogus ticket", msg_p+5, NULL);
return("FAIL invalid ticket");
}
/* The ticket is valid. Release it. */
ticket_array[slot] = TICKET_AVAIL;
num_tickets_out--;
/* Return response */
return("THNX See ya!");
} /* do_goodbye */
/****************************************************************************
* narrate() - chatty news for debugging and logging purposes
*/
void narrate(char *msg1, char *msg2, struct sockaddr_in *clientp)
{
fprintf(stderr,"\t\tSERVER: %s %s ", msg1, msg2);
if ( clientp )
fprintf(stderr,"(%s : %d)", inet_ntoa(clientp->sin_addr),
ntohs(clientp->sin_port));
putc('\n', stderr);
}
程序运行结果
因为进程运行是随机的,所以拿钥匙时结果不是按顺序的。
5.许可证服务器版本2
(1)处理客户端崩溃
服务器必须实现两个独立的操作:等待客户的请求,同时周期性回收丢失的票据。调度行为是简单的,只要使用alarm和signal技术来周期性地调用一个函数。
服务器要收回已经不存在进程的票据,如何判断一个进程是否存在。可以使用popen来运行ps,然后从ps的输出中查找pid是否存在。二是通过给进程发送编号为0的信号以确定它是否存在,如果进程不存在,内核将不会发送信号,而是返回错误并设置errno为ESRCH。
在lserv2.c文件中
/****************************************************************************
* lsrv2.c
* License server server program version 2 - features ticket recycling
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/errno.h>
#define MSGLEN 128
#define RECLAIM_INTERVAL 5 /* reclaim every 60 seconds */
int main(int ac, char *av[])
{
struct sockaddr client_addr;
socklen_t addrlen;
char buf[MSGLEN];
int ret, sock;
void ticket_reclaim(); /* version 2 addition */
unsigned time_left;
sock = setup();
signal(SIGALRM, ticket_reclaim); /* run ticket reclaimer */
alarm(RECLAIM_INTERVAL); /* after this delay */
while(1) {
addrlen = sizeof(client_addr);
ret = recvfrom(sock,buf,MSGLEN,0,
(struct sockaddr *)&client_addr,&addrlen);
if ( ret != -1 ){
buf[ret] = '\0';
narrate("GOT:", buf, &client_addr);
time_left = alarm(0);
handle_request(buf,&client_addr,addrlen);
alarm(time_left);
}
else if ( errno != EINTR )
perror("recvfrom");
}
}
alarm(0)返回剩余的时间,此时闹钟会暂停,alarm(time_left)继续计时剩下的时间,这样做是为了防止客户请求和SIGALAM处理函数对共享数据的处理发生冲突。
lserv_funcs2.c文件的修改
/****************************************************************************
* ticket_reclaim
* go through all tickets and reclaim ones belonging to dead processes
* Results: none
*/
#define RECLAIM_INTERVAL 60 /* Expire every 60 seconds */
void ticket_reclaim()
{
int i;
char tick[BUFSIZ];
for(i = 0; i < MAXUSERS; i++) {
if((ticket_array[i] != TICKET_AVAIL) &&
(kill(ticket_array[i], 0) == -1) && (errno == ESRCH)) {
/* Process is gone - free up slot */
sprintf(tick, "%d.%d", ticket_array[i],i);
narrate("freeing", tick, NULL);
ticket_array[i] = TICKET_AVAIL;
num_tickets_out--;
}
}
alarm(RECLAIM_INTERVAL); /* reset alarm clock */
}
(2)处理服务器崩溃
客户和服务器必须增加票据验证
lclnt2.c程序
/****************************************************************************
* lclnt2.c
* License server client version 2
* link with lclnt_funcs2.o dgram.o
*/
#include<stdio.h>
#include<stdlib.h>
void do_regular_work();
int main(int ac, char *av[])
{
setup();
if (get_ticket() != 0 )
exit(0);
do_regular_work();
release_ticket();
shut_down();
}
/****************************************************************************
* do_regular_work the main work of the application goes here
*/
void do_regular_work()
{
printf("SuperSleep version 1.0 Running - Licensed Software\n");
sleep(15); /* our patented sleep algorithm */
if ( validate_ticket() != 0 ){
printf("Server errors. Please Try later.\n");
return;
}
sleep(15);
}
lclnt_funcs2.c客户端的验证请求
int validate_ticket()
{
char *response;
char buf[MSGLEN];
if(!have_ticket) /* bizarre */
return(0);
sprintf(buf, "VALD %s", ticket_buf); /* compose request */
if ( (response = do_transaction(buf)) == NULL )
return(-1);
narrate("Validated ticket: ", response);
if ( strncmp(response, "GOOD", 4) == 0 )
return(0);
if ( strncmp(response,"FAIL",4) == 0){
have_ticket = 0;
return(-1);
}
narrate("Unknown message:", response);
return(-1);
}
lserv_funcs2.c服务器端验证回应
/****************************************************************************
* handle_request(request, clientaddr, addrlen)
* branch on code in request
*/
void handle_request(char *req,struct sockaddr *client, socklen_t addlen)
{
char *response;
int ret;
/* act and compose a response */
if ( strncmp(req, "HELO", 4) == 0 )
response = do_hello(req);
else if ( strncmp(req, "GBYE", 4) == 0 )
response = do_goodbye(req);
else if ( strncmp(req, "VALD", 4) == 0 )
response = do_validate(req);
else
response = "FAIL invalid request";
/* send the response to the client */
narrate("SAID:", response, client);
ret = sendto(sd, response, strlen(response),0, client, addlen);
if ( ret == -1 )
perror("SERVER sendto failed");
}
/****************************************************************************
* do_validate
* Validate client's ticket
* IN msg_p message received from client
* Results: ptr to response
*/
static char *do_validate(char *msg)
{
int pid, slot; /* components of ticket */
/* msg looks like VAD pid.slot - parse it and validate */
if (sscanf(msg+5,"%d.%d",&pid,&slot)==2 && ticket_array[slot] == pid)
return("GOOD Valid ticket");
/* bad ticket */
narrate("Bogus ticket", msg+5, NULL);
return("FAIL invalid ticket");
}
6.Unix域socket
本地地址通常叫做Unix域地址,它是一个文件名,没有主机和端口号。
为了学习Unix域socket的客户/服务器编程,这里编写了一个日志系统。这里的日志系统使用unix域socket地址。只有同一台主机上的客户才能发消息给它。
/**********************************************************************
* logfilec.c - logfile client - send messages to the logfile server
* usage: logfilec "a message here"
*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<stdlib.h>
#define SOCKET "/tmp/logfilesock"
#define oops(m,x) { perror(m); exit(x); }
main(int ac, char *av[])
{
int sock;
struct sockaddr_un addr;
socklen_t addrlen;
char sockname[] = SOCKET ;
char *msg = av[1];
if ( ac != 2 ){
fprintf(stderr,"usage: logfilec 'message'\n");
exit(1);
}
sock = socket(PF_UNIX, SOCK_DGRAM, 0);
if ( sock == -1 )
oops("socket",2);
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, sockname);
addrlen = strlen(sockname) + sizeof(addr.sun_family) ;
if ( sendto(sock,msg, strlen(msg), 0,
(struct sockaddr *)&addr, addrlen) == -1 )
oops("sendto",3);
}
/*************************************************************************
* logfiled.c - a simple logfile server using Unix Domain Datagram Sockets
* usage: logfiled >>logfilename
*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<time.h>
#include<stdlib.h>
#define MSGLEN 512
#define oops(m,x) { perror(m); exit(x); }
#define SOCKNAME "/tmp/logfilesock"
int main(int ac, char *av[])
{
int sock; /* read messages here */
struct sockaddr_un addr; /* this is its address */
socklen_t addrlen;
char msg[MSGLEN];
int l;
char sockname[] = SOCKNAME ;
time_t now;
int msgnum = 0;
char *timestr;
/* build an address */
addr.sun_family = AF_UNIX; /* note AF_UNIX */
strcpy(addr.sun_path, sockname); /* filename is address */
addrlen = strlen(sockname) + sizeof(addr.sun_family);
sock = socket(PF_UNIX, SOCK_DGRAM, 0); /* note PF_UNIX */
if ( sock == -1 )
oops("socket",2);
/* bind the address */
if ( bind(sock, (struct sockaddr *) &addr, addrlen) == -1 )
oops("bind", 3);
/* read and write */
while(1)
{
l = read(sock, msg, MSGLEN); /* read works for DGRAM */
msg[l] = '\0'; /* make it a string */
time(&now);
timestr = ctime(&now);
timestr[strlen(timestr)-1] = '\0'; /* chop newline */
printf("[%5d] %s %s\n", msgnum++, timestr, msg);
fflush(stdout);
}
}
程序输出结果
小结,两种socket和两种socket地址