Lab Week 09 实验报告
实验内容: 进程间通信 — 管道和 socket 通信
- 编译运行课件 Lecture11 示例代码 alg.11-1, alg.11-2.1, alg.11-2.2,指出你认为不合适的地方并加以改进。
I. Socket 通信
在Socket网络编程中,有如下图所示的流程:
![image-20220409174357295](https://lecture9-1301936037.cos.ap-guangzhou.myqcloud.com/202204091744043.png)
Socket网络编程主要是在两个网络结点(Socket)间建立联系:Client和Server,以便于相互沟通,其中Server以指定的IP地址和端口号上监听(listen),Client则主动地与Server建立连接(connect)。
下面这张图也是相同的原理:
![image-20220409182834905](https://lecture9-1301936037.cos.ap-guangzhou.myqcloud.com/202204091828959.png)
0.通信协议
-
TCP(传输控制协议,TCP,Transmission Control Protocol)
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
-
UDP(用户数据报协议,UDP,User Datagram Protocol))
UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。
1.相关函数
【函数说明参考 man7 Linux manual pages: section 3 的函数Reference】
-
socket()
(头文件<sys/socket.h>)函数原型:
int socket(int __domain, int __type, int __protocol)
扫描二维码关注公众号,回复: 14715176 查看本文章函数功能: 创建socket,且返回一个文件描述符fd,标识唯一的socket;
domain参数指定了通信协议族,如AF_INET:IPv4协议、AF_INET6:IPv6协议;
type参数指定了Socket的类型,如SOCK_STREAM:字节流 TCP(reliable, connection oriented)、
SOCK_DGRAM:UDP(unreliable, connectionless)
protocol参数指定传输协议,通常设置为0(系统根据type来自动选择),也有一些常用的协议IPPROTO_TCP:TCP传输 协议、IPPTOTO_UDP:UDP传输协议;
2.bind()
(头文件<sys/socket.h>)
函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
函数功能: 分配给sockfd指定的socket具体的地址,由socket()
创建的socket最初是未命名的,只有协议族进行标识,
如:IPv4协议的socket则是将IPv4的地址和端口号组合赋给socket。
函数的三个参数分别为:
- sockfd:通过
socket()
创建的socket所返回的文件描述符 - addr: const struct sockaddr类型的指针,指向要绑定给sockfd的协议地址(IP+端口号),IPv4对应的是:
struct in_addr {
unsigned long s_addr;
};
struct sockaddr_in {
short int sin_family; /* 2 bytes, AF_INET for ipv4 */
unsigned short int sin_port; /* 2 bytes, Port number */
struct in_addr sin_addr; /* 4 bytes, IP address */
unsigned char sin_zero[8]; /* padding with 0 to keep the same size as
struct sockaddr (16bytes) */
};
在调用bind之前,需要初始化参数sin_family
, sin_port
, sin_addr
,分别对应socket的通信协议、端口号和IP地址。
注意,如果将端口号设置成0,那么操作系统会随机给程序分配一个可用的侦听端口。
- addrlen:addr的长度;
返回值: 成功返回0,失败返回-1
【以下函数涉及到类型转换】
htonl()
(头文件<sys/socket.h>) 其中的字母对应, h:host, n:net, l:long
这里就涉及到了主机字节序(HBO,Host Byte Order) 和 网络字节序NBO(Network Byte Order) 的概念:
其中,主机字节序为用户的主机中数据的存储方式为大端或者小端,网络字节序一般由通讯协议指定,如,UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节,即高位字节存放在低地址处。由此可见,多字节数值在发送之前,在内存中是以大端方式存放的;
函数原型: uint32_t htonl(uint32_t __hostlong)
函数功能: 将hostlong即主机字节序(与机器相关)转换成网络字节序(大端)
ntohl()
(头文件<sys/socket.h>) net to host long
函数原型:int getsockname(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len)
函数功能: 将一个无符号长整形数从网络转换为主机字节顺序。
inet_ntoa()
(头文件<arpa/inet.h>) n: numeric(数值,地址的二进制值)
函数原型:char *inet_ntoa (struct in_addr);
函数功能: 将网络地址(internet number)转换成“.”点隔的字符串格式,并返回点分十进制的字符串在静态内存中的指针。
IPV4中用4个字节表示一个IP地址,每个字节按照十进制表示为0~255。
点分十进制就是用4个从0~255的数字,来表示一个IP地址。 如192.168.1.1
-
inet_pton()
(头文件<arpa/inet.h>) p: presentation(表达,地址格式), n: numeric(数值,地址的二进制值)(Convert IPv4 and IPv6 addresses from text to binary)
函数原型:
int inet_pton(int family, const char *strptr, void *addrptr);
函数功能: 这个函数与下方的
inet_ntop()
函数互为逆变换,该函数主要是将IPv4或者IPv6格式的地址(由参数af指定,即AF_INET 或者AF_INET6,该地址保存在在strptr中)转换成对应的二进制值并保存在指针addrptr指向的空间中。 -
inet_ntop()
(头文件<arpa/inet.h>)函数原型:
const char *inet_ntop(int af, const void *addrptr, char *strptr, size_t len)
函数功能: 该函数主要是将IPv4或者IPv6格式的地址(由参数af指定,即AF_INET或者AF_INET6)对应的二进制值(保存在指针 addrptr中)转换成IPv4或者IPv6地址格式,并存于指针strptrt指向的空间中,参数len用来表示目标存储单元的大小。
getsocketname()
(头文件<sys/socket.h>)
函数原型:int getsockname(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len)
函数功能: 如果在调用bind()前,在addr结构体中初始化的端口号为0(即由系统随机分配),那么想获得这个端口号就需要调用该 函数,参数中的restrict address
对应的是在bind()中的addr,调用完成之后端口号会被存放在结构体内的sin_port参 数中。
返回值: 成功返回0,失败返回-1
setsockopt()
(头文件<sys/socket.h>)
函数原型:int setsockopt( int socket, int level, int option_name, const void *option_value, size_t ,ption_len)
函数功能: 为socket提供多种选项,如SO_REUSEADDR、SO_REUSEPORT能够让IP地址或者端口号进行复用,避免出现如:“address already in use”
的报错。
如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET
listen()
(头文件<sys/socket.h>)
函数原型:int listen(int socket, int backlog)
函数功能: 将指定的socket由主动套接字转换成被动套接字(passive mode),或者说将socket置为connection-mode socket, 这时 候的socket可以接受其他socket的请求,其中,请求的数量由backlog来限定,在socket中维护了一个队列(listen queue),用于处理还没有接受的或者正在进行的连接(pending),这里的backlog限制了这个队列的大小。
listen()
通常在 bind()
之后,accept()
之前调用
-
accept()
(头文件<sys/socket.h>)(It extracts the first connection request on the queue of pending connections for the listening socket)
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
函数功能: 处于监听状态的socket(以sockfd标识)将会等待客户端socket的请求,并等到请求到来时进行接受,在参数中的addr指针 用于存放客户端socket的信息,包括IP地址和端口号,addrlen为addr的大小
返回值: 返回接受成功的socket的sockfd
connect()
(头文件<sys/socket.h>)
函数原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen)
函数功能: 用于将客户端(Client)与服务端(Server)建立连接,参数sockfd对应的是客户端的描述符,serv_addr指针中为服务端的 信息,包括sin_family、sin_port、sin_addr
send()
(头文件<sys/socket.h>)
函数原型:ssize_t send(int __fd, const void __*buf, size_t __n, int __flags)
函数功能: 从buf中发送n bytes的消息给fd指定的socket
返回值: 返回发送成功的数量,失败返回-1
recv()
(头文件<sys/socket.h>)
函数原型:ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)
函数功能: 从fd指定的socket中读取n bytes到buf中
返回值: 返回读取成功的数量,失败返回-1
getifaddrs()
(头文件<ifaddrs.h>)
函数原型:int getifaddrs(struct ifaddrs **ifap);
函数功能: 用于创建一个描述本地系统网络接口的结构链表,并将列表第一项的地址存储在*ifap中。该列表由ifaddrs结构体组成,定义如下:
struct ifaddrs {
/* interface address, link list structured */
struct ifaddrs *ifa_next; /* next item in link list */
char *ifa_name; /* name of interface */
unsigned int ifa_flags; /* Flags from SIOCGIFFLAGS */
struct sockaddr *ifa_addr; /* address of interface */
struct sockaddr *ifa_netmask; /* netmask of interface */
union {
struct sockaddr *ifu_broadaddr;
/* broadcast address of interface */
struct sockaddr *ifu_dstaddr;
/* point-to-point destination address */
} ifa_ifu;
#define ifa_broadaddr ifa_ifu.ifu_broadaddr
#define ifa_dstaddr ifa_ifu.ifu_dstaddr
void *ifa_data; /* address-specific data */
};
2.相关数据结构
in_addr结构体:
struct in_addr {
unsigned long s_addr;
};
sockaddr结构体:
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
在sockaddr结构体中,存在一定的缺陷,sa_data把目标地址和端口信息混在一起,而下面的sockaddr_in结构体则解决了这个缺陷;
sockaddr_in结构体:
struct sockaddr_in {
short int sin_family; /* 2 bytes, AF_INET for ipv4 */
unsigned short int sin_port; /* 2 bytes, Port number */
struct in_addr sin_addr; /* 4 bytes, IP address */
unsigned char sin_zero[8]; /* padding with 0 to keep the same size as struct sockaddr (16bytes) */
};
sockaddr_in结构体把port和addr 分开储存在两个变量中,分别是sin_port和sin_addr;
sockaddr和sockaddr_in大小一样,都是16 bytes,其中有2 bytes用于地址族,在sockaddr_in中可以发现,多出来sin_zero这个变量,它主要是为了与sockaddr保持相同的size,在使用时通常将其置零。
在网络编程的时候,需要使用accept,getsockname,bind,connect等函数; 只需要记住,填值的时候使用sockaddr_in结构,而作为函数的参数传入的时候转换成sockaddr结构就行了,毕竟都是16个bytes。
【获取网络接口地址】
ifaddrs结构体:
struct ifaddrs {
/* interface address, link list structured */
struct ifaddrs *ifa_next; /* next item in link list */
char *ifa_name; /* name of interface */
unsigned int ifa_flags; /* Flags from SIOCGIFFLAGS */
struct sockaddr *ifa_addr; /* address of interface */
struct sockaddr *ifa_netmask; /* netmask of interface */
union {
struct sockaddr *ifu_broadaddr;
/* broadcast address of interface */
struct sockaddr *ifu_dstaddr;
/* point-to-point destination address */
} ifa_ifu;
#define ifa_broadaddr ifa_ifu.ifu_broadaddr
#define ifa_dstaddr ifa_ifu.ifu_dstaddr
void *ifa_data; /* address-specific data */
};
hostent结构体:(host entries)
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
3.例程代码及说明
A. 获取IP地址和端口号:
alg.11-1-socket-port.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* #include <arpa/inet.h>
uni32_t htonl(uni32_t hostlong); // host to net long int
uni16_t htonl(uni16_t hostshort); // host to net short int
uni32_t ntohl(uni32_t netlong); // net to host long int
uni16_t ntohl(uni16_t netshort); // net to host short int
unsigned long inet_addr(const char* cp); // cp e.g., "192.168.10.130"
char* inet_ntoa(struct in_addr in);
*/
int main(void)
{
unsigned short port = 0;
int sockfd, ret, ret_val = 1;
struct sockaddr_in myaddr;
socklen_t addr_len;
char ip_name_str[INET_ADDRSTRLEN];
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* AF_INET: ipv4 */
if(sockfd == -1) {
perror("socket()");
return EXIT_FAILURE;
}
myaddr.sin_family = AF_INET; /* ipv4 */
myaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* INADDR_ANY: 0 */
myaddr.sin_port = 0; /* if .sin_port is set to 0, a free port number allocated by bind() */
addr_len = sizeof(myaddr);
ret = bind(sockfd, (struct sockaddr *)&myaddr, addr_len);
/* bind() not return the port number to myaddr: bind(int, const struct sockaddr, socklen_t)
use getsockname() to get the allocated port number */
if(ret == 0) {
addr_len = sizeof(myaddr);
ret = getsockname(sockfd, (struct sockaddr *)&myaddr, &addr_len);
if(ret == 0) {
port = ntohs(myaddr.sin_port);
printf("port number = %d\n", port);
strcpy(ip_name_str, inet_ntoa(myaddr.sin_addr));
printf("host addr = %s\n", ip_name_str);
} else {
ret_val = 0;
}
} else {
ret_val = 0;
}
ret = close(sockfd); /* close() defined in <unistd.h> */
if(ret != 0) {
ret_val = 0;
}
return ret_val;
}
代码说明:
该程序主要是以IPv4作为通信协议,利用socket()
函数创建了一个socket,在调用bind()
函数对socket进行绑定前,需要在sockaddr_in
类型的结构体中对相关的参数赋值(需要将主机字节序转换成网络字节序,利用 htonl()
),包括通信协议族、IP地址和端口号(在例程代码中置为0,由bind()进行分配),进而调用bind()
函数将该结构体中的内容与socket进行绑定,由于例程代码中的端口号是随机分配的,因此需要调用getsockname()
函数来获得随机分配到的端口号;
需要注意此时获得的IP地址和端口号都是按照网络字节序(大端方式)进行存储的,因此需要调用 ntohs()
将端口号转换成主机字节序(通常我们都是用的小端机器,如果是大端机器就无所谓了),IP地址需要进行格式转换,将二进制的格式的无符号长整型数转换成点分十进制的字符串,这就便于用户进行识别。
最后将IP地址和端口号打印出来。
运行结果:
发现系统分配的端口号为45117, 而IP地址由于指定的是INADDR_ANY
,因此最后分配到的是0.0.0.0
B. Server&Client
这部分的流程在开头的图中已经有了详细的说明,接下来的代码就是针对Server和Client直接的通信进行实现。
例程代码:
alg.11-2.1-socket-server.c
/* one client, one server, asynchronous send-receive */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <sys/signal.h>
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
#define ERR_EXIT(m) \
do {
\
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
/* get a string of length n-1 from stdin and clear the stdin buffer */
char* s_gets(char* stdin_buf, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(stdin_buf, n, stdin);
if(ret_val) {
while(stdin_buf[i] != '\n' && stdin_buf[i] != '\0') {
i++;
}
if(stdin_buf[i] == '\n') {
stdin_buf[i] = '\0';
} else {
while (getchar() != '\n') ;
}
}
return ret_val;
}
/* save the ipv4 address of this server to ip_addr */
int getipv4addr(char *ip_addr)
{
struct ifaddrs *ifaddrsptr = NULL;
struct ifaddrs *ifa = NULL;
void *tmpptr = NULL;
int ret;
ret = getifaddrs(&ifaddrsptr);
if (ret == -1) {
ERR_EXIT("getifaddrs()");
}
for(ifa = ifaddrsptr; ifa != NULL; ifa = ifa->ifa_next) {
if(!ifa->ifa_addr) {
continue;
}
if(ifa->ifa_addr->sa_family == AF_INET) {
/* AF_INET: ipv4 */
tmpptr = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
char addr_buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, tmpptr, addr_buf, INET_ADDRSTRLEN); // 将二进制格式转换成IPv4协议的点分十进制格式
printf("%s ipv4 address = %s\n", ifa->ifa_name, addr_buf);
if (strcmp(ifa->ifa_name, "lo") != 0) {
// lo: local,系统虚拟的环回接口
strcpy(ip_addr, addr_buf); /* save the ipv4 address */
}
} else if(ifa->ifa_addr->sa_family == AF_INET6) {
/* ipv6 */
tmpptr = &((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
char addr_buf[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, tmpptr, addr_buf, INET6_ADDRSTRLEN);
printf("%s ipv6 address %s\n", ifa->ifa_name, addr_buf);
}
}
if (ifaddrsptr != NULL) {
freeifaddrs(ifaddrsptr);
}
return EXIT_SUCCESS;
}
int main(void)
{
int server_fd, connect_fd;
struct sockaddr_in server_addr, connect_addr;
socklen_t addr_len;
int recvbytes, sendbytes, ret;
char send_buf[BUFFER_SIZE], recv_buf[BUFFER_SIZE];
char ip_addr[INET_ADDRSTRLEN]; /* ipv4 address */
char stdin_buf[BUFFER_SIZE];
uint16_t port_num;
pid_t childpid;
server_fd = socket(AF_INET, SOCK_STREAM, 0); /* create an ipv4 server*/
if(server_fd == -1) {
ERR_EXIT("socket()");
}
printf("server_fd = %d\n", server_fd);
ret = getipv4addr(ip_addr); /* get server ipv4 address */
if (ret == EXIT_FAILURE) {
ERR_EXIT("getifaddrs()");
}
/* set sockaddr_in */
server_addr.sin_family = AF_INET; /* ipv4 */
server_addr.sin_port = 0; /* auto server port number */
server_addr.sin_addr.s_addr = inet_addr(ip_addr); /* unsigned long s_addr */
bzero(&(server_addr.sin_zero), 8); /* padding with 0s */
int opt_val = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val)); /* many options */
addr_len = sizeof(struct sockaddr);
ret = bind(server_fd, (struct sockaddr *)&server_addr, addr_len);
if(ret == -1) {
close(server_fd);
ERR_EXIT("bind()");
}
printf("Bind success!\n");
addr_len = sizeof(server_addr);
ret = getsockname(server_fd, (struct sockaddr *)&server_addr, &addr_len);
if(ret == 0) {
printf("Server port number = %d\n", ntohs(server_addr.sin_port)); // ntohs: net to host (short: 2 bytes)
} else {
close(server_fd);
ERR_EXIT("Server port number unknownn");
}
ret = listen(server_fd, MAX_QUE_CONN_NM);
if(ret == -1) {
close(server_fd);
ERR_EXIT("listen()");
}
printf("Server ipv4 addr: %s\n", ip_addr);
printf("Listening ...\n");
addr_len = sizeof(struct sockaddr);
/* addr_len should be refreshed before each accept() */
connect_fd = accept(server_fd, (struct sockaddr *)&connect_addr, &addr_len);
/* waiting for connection */
if(connect_fd == -1) {
close(server_fd);
ERR_EXIT("accept()");
}
port_num = ntohs(connect_addr.sin_port);
strcpy(ip_addr, inet_ntoa(connect_addr.sin_addr));
printf("Client accepted: IP addr = %s, port = %hu\n", ip_addr, port_num);
childpid = fork();
if(childpid < 0) {
close(server_fd);
close(connect_fd);
ERR_EXIT("fork()");
}
if(childpid > 0) {
/* parent pro */
while(1) {
/* sending cycle */
memset(send_buf, 0, BUFFER_SIZE);
s_gets(send_buf, BUFFER_SIZE); /* without endding '\n' */
sendbytes = send(connect_fd, send_buf, strlen(send_buf), 0); /* blocking send */
if(sendbytes <= 0) {
printf("sendbytes = %d. Connection terminated ...\n", sendbytes);
break;
}
if(strncmp(send_buf, "end", 3) == 0) {
break;
}
}
close(server_fd);
close(connect_fd);
kill(childpid, SIGKILL);
}
else {
/* child pro */
while(1) {
/* receiving cycle */
memset(recv_buf, 0, BUFFER_SIZE);
recvbytes = recv(connect_fd, recv_buf, BUFFER_SIZE, 0); /* blocking receive */
if(recvbytes <= 0) {
printf("recvbytes = %d. Connection terminated ...\n", recvbytes);
break;
}
printf("\t\t\t\t\tClient %s >>>> %s\n", ip_addr, recv_buf);
if(strncmp(recv_buf, "end", 3) == 0) {
break;
}
}
close(connect_fd);
close(server_fd);
kill(getppid(), SIGKILL);
}
return EXIT_SUCCESS;
}
代码说明:
这部分起到服务端(server)的作用,还是采用IPv4协议创建一个socket,这个socket作为服务端。
主要流程:socket()
-> bind()
-> listen()
-> accept()
到了这里之后,需要等待client端的连接,连接成功后,可以通过系统调用send()
和recv()
来进行消息的发送和接收。
接下来,对程序中的细节部分进行详细的说明:
- 获取基于IPv4协议的IP地址: 程序中封装了函数
getipv4addr()
用于获取本地的IPv4地址,这个函数中用到了getifaddrs()
来创建一个描述本地系统网络接口的结构链表,从中得到IP地址并保存在char型数组ip_addr
中。 - 将地址信息置入struct sockaddr_in类型的结构体中: 代码中定义了
struct sockaddr_in server_addr
,在该结构体中,需要指定通信协议、IP地址、端口号,同时,在对这些参数进行赋值的同时需要进行类型转换,如IP地址,需要调用inet_addr()
函数将点分十进制的IP地址格式转换成unsigned long 无符号长整形,在这里,端口号的值仍然赋值为0,由系统指派端口号。 - 将IP地址和端口号信息打印出来: 通过调用
getsockname()
,可以将系统分配的端口号置入server_addr
中,将地址信息打印出来,方便让client进行连接。 - listen和accept: 当上述准备工作做好之后,server需要进入监听状态,并且等待client的连接请求,连接成功后,
accept()
函数会将client的地址信息置入struct sockaddr_in
类型的结构体connect_addr
中,并将其打印出来,注意,在这之前仍然需要进行类型转换。accept()
函数的返回值是client 的sockfd,即socket文件描述符,唯一地标识了client。 - 进行消息传递: 连接成功后,就可以在server和client端进行通信了,在代码中主要是利用了
fork()
来创建一个子进程,其中父进程负责读取键盘输入的信息并将其发送给client,子进程负责接收client发送过来的信息,注意这里的父进程和子进程是同时并发执行的。
例程代码:
alg.11-2.2-socket-client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 1024
#define ERR_EXIT(m) \
do {
\
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
/* get a string of length n-1 from stdin and clear the stdin buffer */
char* s_gets(char* stdin_buf, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(stdin_buf, n, stdin);
if(ret_val) {
while(stdin_buf[i] != '\n' && stdin_buf[i] != '\0') {
i++;
}
if(stdin_buf[i] == '\n') {
stdin_buf[i] = '\0';
} else {
while (getchar() != '\n') ;
}
}
return ret_val;
}
int main(void)
{
int connect_fd, sendbytes, recvbytes, ret;
uint16_t port_num;
char send_buf[BUFFER_SIZE], recv_buf[BUFFER_SIZE];
char ip_addr[INET_ADDRSTRLEN], stdin_buf[BUFFER_SIZE];
char clr;
struct hostent *host;
struct sockaddr_in server_addr, connect_addr;
socklen_t addr_len;
pid_t childpid;
printf("Input server's hostname/ipv4: "); /* www.baidu.com or an ipv4 address */
scanf("%s", stdin_buf);
while((clr = getchar()) != '\n' && clr != EOF); /* clear the stdin buffer */
printf("Input server's port number: ");
scanf("%hu", &port_num);
while((clr = getchar()) != '\n' && clr != EOF);
host = gethostbyname(stdin_buf);
if(host == NULL) {
printf("invalid name or ip address\n");
exit(EXIT_FAILURE);
}
printf("server's official name = %s\n", host->h_name);
char** ptr = host->h_addr_list;
for(; *ptr != NULL; ptr++) {
inet_ntop(host->h_addrtype, *ptr, ip_addr, sizeof(ip_addr));
printf("\tserver address = %s\n", ip_addr);
}
/*creat connection socket*/
connect_fd = socket(AF_INET, SOCK_STREAM, 0);
if(connect_fd == -1) {
ERR_EXIT("socket()");
}
/* set sockaddr_in of server-side */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port_num);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(server_addr.sin_zero), 8);
addr_len = sizeof(struct sockaddr);
ret = connect(connect_fd, (struct sockaddr *)&server_addr, addr_len); /* connect to server */
if(ret == -1) {
close(connect_fd);
ERR_EXIT("connect()");
}
/* connect_fd is allocated a port_number after connecting */
addr_len = sizeof(struct sockaddr);
ret = getsockname(connect_fd, (struct sockaddr *)&connect_addr, &addr_len);
if(ret == -1) {
close(connect_fd);
ERR_EXIT("getsockname()");
}
port_num = ntohs(connect_addr.sin_port);
strcpy(ip_addr, inet_ntoa(connect_addr.sin_addr));
printf("local socket ip addr = %s, port = %hu\n", ip_addr, port_num);
childpid = fork();
if(childpid < 0) {
close(connect_fd);
ERR_EXIT("fork()");
}
if(childpid > 0) {
/* parent pro */
while(1) {
/* sending cycle */
memset(send_buf, 0, BUFFER_SIZE);
s_gets(send_buf, BUFFER_SIZE); /* without endding '\n' */
sendbytes = send(connect_fd, send_buf, strlen(send_buf), 0); /* blocking send */
if(sendbytes <= 0) {
printf("sendbytes = %d. Connection terminated ...\n", sendbytes);
break;
}
if(strncmp(send_buf, "end", 3) == 0)
break;
}
close(connect_fd);
kill(childpid, SIGKILL);
}
else {
/* child pro */
while(1) {
/* receiving cycle */
memset(recv_buf, 0, BUFFER_SIZE);
recvbytes = recv(connect_fd, recv_buf, BUFFER_SIZE, 0); /* blocking receive */
if(recvbytes <= 0) {
printf("recvbytes = %d. Connection terminated ...\n", recvbytes);
break;
}
printf("\t\t\t\t\tserver %s >>>> %s\n", ip_addr, recv_buf);
if(strncmp(recv_buf, "end", 3) == 0)
break;
}
close(connect_fd);
kill(getppid(), SIGKILL);
}
return EXIT_SUCCESS;
}
代码说明:
这部分代码主要是client端的实现,在接收和发送信息的部分与server端的实现相同。
主要流程:socket()
-> connect()
连接成功后,可以通过系统调用send()
和recv()
来进行消息的发送和接收。
接下来,对程序中的细节部分进行详细的说明:
- 获取server端的信息: 在代码开头定义了
struct hostent
类型的结构体指针host
,在这个结构体中封装了与host相关的(在这里是server端)信息,如:official name,host address type等等,获取到server的IP地址或者hostname后,再通过调用gethostbyname()
函数可以获得server端的struct hostent
类型的结构体指针,代码中打印出了server端的official name。 - 创建socket并与server端建立连接: 在client端最重要的任务,就是与server端建立连接,通过调用
socket()
函数创建client端的socket之后,还需要调用connect()
函数来与server端进行连接,在代码开头定义了struct sockaddr_in
类型的结构体指针server_addr
,在调用connect()
函数之前,需要将server端的信息都填入该结构体当中,当然,不要忘了类型转换。
注意到:bzero(&(server_addr.sin_zero), 8)
,这里的bzero()
函数用于将server_addr.sin_zero
前8个字节清零,
其实还有另外一个函数能实现这个功能:memset()
区别:
- bzero()不是ANSI C函数,其起源于早期的Berkeley网络编程代码,但是几乎所有支持套接字API的厂商都提供该函数;
- memset()为ANSI C函数,更常规、用途更广。
运行结果:
初始状态:
从服务端的输出可以看到,通过遍历ifaddrs结构体链表中的每一个结点,可以获得本地系统网络接口中的信息,其中,存在接口名称为lo 的 ipv4 address = 127.0.0.1, enp129s0f0的 ipv4 address = 172.16.86.52, docker0的ipv4 address = 172.17.0.1, 还有接口名称为lo 的ipv6 address ::1,enp129s0f0的ipv6 address fe80::ae1f:6bff:fe26:d546;
此时程序中会以docker0的IPv4地址作为server的IP地址,即172.17.0.1,同时,分配到的端口号为33942;
随后服务端server调用 listen()
进入监听状态,并且调用accept()
函数,等待客户端client的连接。
现在在客户端中输入服务端的IP地址和端口号:
在server端中显示了连接的client端的IP地址和端口号,在client端中显示了server端的official name 和server address 都是172.17.0.1,和client端自己的IP地址和端口号。
在client端中向server端发送一条信息:hello1, 在server端中显示接收到信息。
再发两条进行测试:
现在反过来,由server来发送信息:
server端发送一条信息:hello1 client, client端中显示接收到信息。
再次测试:
client端中成功接收到信息。
现在来终止连接:
在client端中键入end后,client端退出进程,随后server端被kill掉。
4.代码改进:
在以上代码的基础上,实现FTP(File TransferProtocol)文件传输协议,让client端能从server端下载文件:
相关代码:
server_FTP.c
/* one client, one server, asynchronous send-receive */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <sys/signal.h>
#include <sys/stat.h>
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
#define ERR_EXIT(m) \
do {
\
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
/* get a string of length n-1 from stdin and clear the stdin buffer */
char* s_gets(char* stdin_buf, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(stdin_buf, n, stdin);
if(ret_val) {
while(stdin_buf[i] != '\n' && stdin_buf[i] != '\0') {
i++;
}
if(stdin_buf[i] == '\n') {
stdin_buf[i] = '\0';
} else {
while (getchar() != '\n') ;
}
}
return ret_val;
}
/* save the ipv4 address of this server to ip_addr */
int getipv4addr(char *ip_addr)
{
struct ifaddrs *ifaddrsptr = NULL;
struct ifaddrs *ifa = NULL;
void *tmpptr = NULL;
int ret;
ret = getifaddrs(&ifaddrsptr);
if (ret == -1) {
ERR_EXIT("getifaddrs()");
}
for(ifa = ifaddrsptr; ifa != NULL; ifa = ifa->ifa_next) {
if(!ifa->ifa_addr) {
continue;
}
if(ifa->ifa_addr->sa_family == AF_INET) {
/* AF_INET: ipv4 */
tmpptr = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
char addr_buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, tmpptr, addr_buf, INET_ADDRSTRLEN); // 将二进制格式转换成IPv4协议的点分十进制格式
printf("%s ipv4 address = %s\n", ifa->ifa_name, addr_buf);
if (strcmp(ifa->ifa_name, "lo") != 0) {
strcpy(ip_addr, addr_buf); /* save the ipv4 address */
}
} else if(ifa->ifa_addr->sa_family == AF_INET6) {
/* ipv6 */
tmpptr = &((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
char addr_buf[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, tmpptr, addr_buf, INET6_ADDRSTRLEN);
printf("%s ipv6 address %s\n", ifa->ifa_name, addr_buf);
}
}
if (ifaddrsptr != NULL) {
freeifaddrs(ifaddrsptr);
}
return EXIT_SUCCESS;
}
int main(void)
{
int server_fd, connect_fd;
struct sockaddr_in server_addr, connect_addr;
socklen_t addr_len;
int recvbytes, sendbytes, ret;
char send_buf[BUFFER_SIZE], recv_buf[BUFFER_SIZE];
char ip_addr[INET_ADDRSTRLEN]; /* ipv4 address */
char stdin_buf[BUFFER_SIZE];
uint16_t port_num;
pid_t childpid;
server_fd = socket(AF_INET, SOCK_STREAM, 0); /* create an ipv4 server*/
if(server_fd == -1) {
ERR_EXIT("socket()");
}
printf("server_fd = %d\n", server_fd);
ret = getipv4addr(ip_addr); /* get server ipv4 address */
if (ret == EXIT_FAILURE) {
ERR_EXIT("getifaddrs()");
}
/* set sockaddr_in */
server_addr.sin_family = AF_INET; /* ipv4 */
server_addr.sin_port = 0; /* auto server port number */
server_addr.sin_addr.s_addr = inet_addr(ip_addr); /* unsigned long s_addr */
bzero(&(server_addr.sin_zero), 8); /* padding with 0s */
int opt_val = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof(opt_val)); /* many options */
addr_len = sizeof(struct sockaddr);
ret = bind(server_fd, (struct sockaddr *)&server_addr, addr_len);
if(ret == -1) {
close(server_fd);
ERR_EXIT("bind()");
}
printf("Bind success!\n");
addr_len = sizeof(server_addr);
ret = getsockname(server_fd, (struct sockaddr *)&server_addr, &addr_len);
if(ret == 0) {
printf("Server port number = %d\n", ntohs(server_addr.sin_port)); // ntohs: net to host (short: 2 bytes)
} else {
close(server_fd);
ERR_EXIT("Server port number unknownn");
}
ret = listen(server_fd, MAX_QUE_CONN_NM);
if(ret == -1) {
close(server_fd);
ERR_EXIT("listen()");
}
printf("Server ipv4 addr: %s\n", ip_addr);
printf("Listening ...\n");
addr_len = sizeof(struct sockaddr);
/* addr_len should be refreshed before each accept() */
connect_fd = accept(server_fd, (struct sockaddr *)&connect_addr, &addr_len);
/* waiting for connection */
if(connect_fd == -1) {
close(server_fd);
ERR_EXIT("accept()");
}
port_num = ntohs(connect_addr.sin_port);
strcpy(ip_addr, inet_ntoa(connect_addr.sin_addr));
printf("Client accepted: IP addr = %s, port = %hu\n", ip_addr, port_num);
FILE*fileptr;
struct stat fileattr;
char file[BUFFER_SIZE];
while(1){
memset(recv_buf, 0, BUFFER_SIZE);
memset(file, 0, BUFFER_SIZE);
recvbytes = recv(connect_fd, recv_buf, BUFFER_SIZE, 0); /* blocking receive */
if(recvbytes <= 0) {
printf("recvbytes = %d. Connection terminated ...\n", recvbytes);
break;
}
if(strncmp(recv_buf, "end", 3) == 0) {
break;
}
fileptr = fopen(recv_buf, "r");
if(stat(recv_buf, &fileattr) == 0) {
printf("The filename is: %s\n", recv_buf);
int filesize = fileattr.st_size;
fread(file, sizeof(file), 1, fileptr);
send(connect_fd, file, sizeof(file), 0);
}
else{
printf("File doesn't exsit!\n");
}
}
close(connect_fd);
close(server_fd);
return EXIT_SUCCESS;
}
client_FTP.c.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 1024
#define ERR_EXIT(m) \
do {
\
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
/* get a string of length n-1 from stdin and clear the stdin buffer */
char* s_gets(char* stdin_buf, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(stdin_buf, n, stdin);
if(ret_val) {
while(stdin_buf[i] != '\n' && stdin_buf[i] != '\0') {
i++;
}
if(stdin_buf[i] == '\n') {
stdin_buf[i] = '\0';
} else {
while (getchar() != '\n') ;
}
}
return ret_val;
}
int main(void)
{
int connect_fd, sendbytes, recvbytes, ret;
uint16_t port_num;
char send_buf[BUFFER_SIZE], recv_buf[BUFFER_SIZE];
char ip_addr[INET_ADDRSTRLEN], stdin_buf[BUFFER_SIZE];
char clr;
struct hostent *host;
struct sockaddr_in server_addr, connect_addr;
socklen_t addr_len;
pid_t childpid;
printf("Input server's hostname/ipv4: "); /* www.baidu.com or an ipv4 address */
scanf("%s", stdin_buf);
while((clr = getchar()) != '\n' && clr != EOF); /* clear the stdin buffer */
printf("Input server's port number: ");
scanf("%hu", &port_num);
while((clr = getchar()) != '\n' && clr != EOF);
host = gethostbyname(stdin_buf);
if(host == NULL) {
printf("invalid name or ip address\n");
exit(EXIT_FAILURE);
}
printf("server's official name = %s\n", host->h_name);
char** ptr = host->h_addr_list;
for(; *ptr != NULL; ptr++) {
inet_ntop(host->h_addrtype, *ptr, ip_addr, sizeof(ip_addr));
printf("\tserver address = %s\n", ip_addr);
}
/*creat connection socket*/
connect_fd = socket(AF_INET, SOCK_STREAM, 0);
if(connect_fd == -1) {
ERR_EXIT("socket()");
}
/* set sockaddr_in of server-side */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port_num);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(server_addr.sin_zero), 8);
addr_len = sizeof(struct sockaddr);
ret = connect(connect_fd, (struct sockaddr *)&server_addr, addr_len); /* connect to server */
if(ret == -1) {
close(connect_fd);
ERR_EXIT("connect()");
}
/* connect_fd is allocated a port_number after connecting */
addr_len = sizeof(struct sockaddr);
ret = getsockname(connect_fd, (struct sockaddr *)&connect_addr, &addr_len);
if(ret == -1) {
close(connect_fd);
ERR_EXIT("getsockname()");
}
port_num = ntohs(connect_addr.sin_port);
strcpy(ip_addr, inet_ntoa(connect_addr.sin_addr));
printf("local socket ip addr = %s, port = %hu\n", ip_addr, port_num);
char filename[BUFFER_SIZE];
while(1){
printf("Please enter file name:\n");
memset(send_buf, 0, BUFFER_SIZE);
s_gets(send_buf, BUFFER_SIZE); /* without endding '\n' */
sendbytes = send(connect_fd, send_buf, strlen(send_buf), 0); /* blocking send */
if(sendbytes <= 0) {
printf("sendbytes = %d. Connection terminated ...\n", sendbytes);
break;
}
if(strncmp(send_buf, "end", 3) == 0)
break;
memset(recv_buf, 0, BUFFER_SIZE);
recvbytes = recv(connect_fd, recv_buf, BUFFER_SIZE, 0); /* blocking receive */
if(recvbytes <= 0) {
printf("recvbytes = %d. Connection terminated ...\n", recvbytes);
break;
}
printf("--------%s--------\n",send_buf);
printf("%s\n",recv_buf);
}
close(connect_fd);
return EXIT_SUCCESS;
}
先创建两个txt文件作为测试:
file1.txt
This is file1 -- base on FTP
ALL WORKS AND NO PLAY MAKES JACK A DULL BOY
file2.txt
This is file2 -- base on FTP
All work and no play makes jack a dull boy
在client端输入server端的IP地址和端口号后,输入想要获取的文件名。
client端输入file1.txt后,在client端获取到了文件中的信息:
再输入file2.txt继续进行测试,同样地,在client端获取到了文件中的信息:
最后,client端输入end,作为结束标志:
II.总结:
本次实验主要是基于socket进行网络编程,使用基于IPv4协议的TCP通信协议实现了server端和client端的双向传输,
Server端的主要流程为:socket()
-> bind()
-> listen()
-> accept()
到了这里之后,需要等待client端的连接,连接成功后,可以通过系统调用send()
和recv()
来进行消息的发送和接收;Client端的主要流程为socket()
-> connect()
与Server端连接成功后,再通过系统调用send()
和recv()
来进行消息的发送和接收。