C语言学习笔记——网络编程(2)
本文以网络编程(1) 为基础,对相关内容继续深入学习
主要系统调用函数
字节顺序和转换函数
- 不同机器内部对变量的字节存储顺序不同,有的采用大端模式(big-endian),有的采用小端模式(little-endian),大端模式是指高字节数据存放在低地址处,低字节数据存放在高地址处,例如0x04030201分别在大小端模式下的存储格式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6d2nyKft-1595063603852)(/home/lancibe/桌面/大、小端模式.jpg “大、小端模式”)] - 在网络上传输数据时,由于数据传输的两端可能对应不同的硬件平台,采用的存储字节顺序也可能不一致,因此TCP/IP协议规定了在网络上必须采用网络字节顺序(也就是大端模式)。通过对大小端模式存储原理的分析也可以发现,对于char型数据,由于其只占一个字节,所以不存在这个问题,这也是我们一般把数据缓冲区定义成char型的原因之一。而对于IP地址、端口号等非char型数据,必须在数据发送到网络上之前将其转换成大端模式,在接收到数据之后再将其转换成符合接收端主机的存储模式。Linux系统为大小端模式的转换提供了4个函数,在Shell下输入
man byteorder
可获取他们的函数原型如下:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- htonl表示host to network long,用于将主机unsigned int型数据转换成网络字节数据;htons表示host to network short,用于将主机unsigned short型数据转换成网络字节顺序;ntohl、ntohs的功能分别于htonl、htons相反。
inet系列函数
- 通常我们习惯于使用字符串形式的网络地址(如172.17.242.131),然而在网络上进行数据通信是,需要的是二进制形式且为网络字节顺序的IP地址。Linux系统为网络地址的格式转换提供了一系列函数,在Shell下输入
man inet
可获取他们的函数原型如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
- 函数inet_aton:将参数cp所指向的字符串形式的IP地址转换为二进制的网络字节顺序的IP地址,转换后的结果存于参数imp所指向的空间中。执行成功返回非0值,参数无效则返回0值。
- 函数inet_addr:该函数的功能与inet_aton()类似,他将参数cp所指向的字符串形式的网络地址转换为网络字节顺序形式的二进制IP地址,执行成功时将转换后的结果返回,参数无效返回INADDR_NONE(一般该值为-1)。该函数已经过时,推荐使用inet_aton()。因为对有效地址255.255.255.255他也返回-1,(因为-1的补码形式为0xFFFFFFFF),使得用户可能将255.255.255.255当成无效的非法地址。而使用inet_aton()这不存在这种问题。
- 函数inet_network:将参数cp所指向的字符串形式的网络地址转换为主机字节顺序形式的二进制IP地址,执行成功返回转换后的结果,参数无效返回-1。
- 函数inet_ntoa:该函数将值为in的网络字节顺序的二进制IP地址转换成以**.**分割的字符串形式,执行成功返回结果字符串的指针,参数无效返回NULL。
- 函数inet_makeaddr:该函数将把网络号为参数net,主机号为参数host的两个地址组合成一个网络地址,如net取0xac11(172.17.0.0,主机字节顺序形式),host取0xf283(0.0.242.131,主机字节顺序形式),这组合后的地址为172.17.242.131,并表示为网络字节顺序形式0x83f211ac。
- 函数inet_lnaof:该函数从参数in中提取出主机地址,执行成功返回主机字节顺序形式的主机地址,属于B类地址,则主机号为低16位,主机地址为0.0.242.131,按主机字节顺序输出则为0xf283。
- 函数inet_netof:该函数从参数in中提取出网络地址,执行成功返回主机字节顺序形式的网咯地址。如172.17.242.131,属于B类地址,则高16位表示网络号,网络地址为172.17.0.0,按主机字节顺序输出则为0xac11,下例实现inet函数族的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
char buffer[32];
int ret = 0;
int host = 0;
int network = 0;
unsigned int address = 0;
struct in_addr in;
in.s_addr = 0;
//输入一个以"."分割的字符串形式的IP地址
printf("input your IP address:\n");
fgets(buffer, 31, stdin);
buffer[31] = '\0';
//示例使用inet_aton()函数
if ((ret = inet_aton(buffer, &in)) == 0)
{
printf("inet_aton: \t invalid address\n");
}
else
{
printf("inet_aton:\t0x%x\n", in.s_addr);
}
//示例使用inet_addr()函数
if ((address = inet_addr(buffer)) == INADDR_NONE)
{
printf("inet_addr: \t invalid address\n");
}
else
{
printf("inet_addr:\t0x%x\n", address);
}
//示例使用inet_network()函数
if ((address = inet_network(buffer)) == -1)
{
printf("inet_network: \t invalid address\n");
}
else
{
printf("inet_network:\t0x%x\n", address);
}
//示例使用inet_ntoa()函数
if (inet_ntoa(in) == NULL)
{
printf("inet_ntoa: \t invalid address\n");
}
else
{
printf("inet_ntoa:\t%s\n", inet_ntoa(in));
}
//示例使用inet_lnaof()与inet_netof()函数
host = inet_lnaof(in);
network = inet_netof(in);
printf("inet_lnaof:\t0x%x\n", host);
printf("inet_netof:\t0x%x\n", network);
in = inet_makeaddr(network, host);
printf("inet_makeaddr:\t0x%x\n", in.s_addr);
return 0;
}
- 两次运行程序,得到如下结果
lancibe@lancibe-TM1701:~/c$ ./inet
input your IP address:
172.17.242.131
inet_aton: 0x83f211ac
inet_addr: 0x83f211ac
inet_network: 0xac11f283
inet_ntoa: 172.17.242.131
inet_lnaof: 0xf283
inet_netof: 0xac11
inet_makeaddr: 0x83f211ac
lancibe@lancibe-TM1701:~/c$ ./inet
input your IP address:
255.255.255.255
inet_aton: 0xffffffff
inet_addr: invalid address
inet_network: invalid address
inet_ntoa: 255.255.255.255
inet_lnaof: 0xff
inet_netof: 0xffffff
inet_makeaddr: 0xffffffff
- 从运行结果可以看到,函数inet_addr()和inet_network()把地址255.255.255.255当成了无效地址。
getsockopt()和setsockopt()
- 套接字创建以后,就可以利用它来传输数据,但有时可能对套接字的工作方式有特殊的要求,此时就需要修改套接字的属性。系统提供了套接字选项来控制套接字的属性,使用函数getsockopt可以获取套接字的属性,使用setsockopt()可以设置套接字的属性,在Shell下输入
man getsockopt
可获取他们的原型:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- 参数sockfd是一个套接字,参数level是进行套接字选项操作的层次,可以取SOL_SOCKET(通用套接字)、IPPROTO_IP(IP层套接字)、IPPROTO_TCP(TCP层套接字)等值,一般取第一个来进行与特定协议不相关的操作。参数optname是套接字选项的名称。
- 对于函数getsockopt,参数optval用来存放获得的套接字选项,参数optlen在调用函数前其值为optval指向空间的大小,调用完成后则其值为参数optval所保存的结果的实际大小。对于函数setsockopt,参数optval是待设置的套接字选项的值,参数optlen是该选项的长度。
- 这两个函数执行成功时都返回0,出错都返回-1,错误代码存入errno中。
- 下面介绍通用套接字SOL_SOCKET选项,可以使用命令
man 7 socket
获取更详细的介绍- SO_KEEPALIVE:如果没有设置SO_KEEPALIVE选项,那么即使TCP连接已经很长时间没有数据传输时,系统也不会检测这个连接是否有效,对于服务器进程,如果某一客户端非正常断开连接,则服务器进程将一直被阻塞等待。因此服务器端程序需要使用这个选项,如果某个客户端一段时间内没有反应则关闭该连接
- SO_RCVLOWAT和SO_SNDLOWAT:SO_RCVLOWAT表示接收缓冲区的下限,只有当接收缓冲区中的数据超过了SO_RCVLOWAT才会将数据传送到上层应用程序。SO_SNDLOWAT表示发送缓冲区的下限,只有当发送缓冲区中数据超过了SO_SNDLOWAT才会将数据发送出去。Linux下这两个值都为1且不能更改,也就是说只要有数据就接收或发送。这两个选项只能使用getsockopt函数获取,不能用setsockopt函数更改。
- SO_RCVTIMEO和SO_SNDTIMEO:这两个选项可以设置对套接字读或写的超时时间,具体时间由下面这个结构指定:
struct timeval {
long tv_sec; //秒数
long tv_usec; //微秒数
}
- 成员tv_sec指定秒数,tv_usec指定微秒数。超时时间为这两个时间的和。在某个套接字连接上,若读或写超时,则认为接受或发送数据失败。
- **SO_BINDTODEVICE**:将套接字绑定在特定的网络接口如**eth0**,此后只有该网络接口上的数据才会被套接字处理。如果将选项值设置为空字符串或选项长度设为0将取消绑定。
- **SO_DEBUG**:该选项只能对TCP套接字使用,设置了该选项后系统将保存TCP发送和接收的所有数据的相关信息,以方便调试程序。
- **SO_REUSEADDR**:Linux系统中,如果一个socket绑定了一个端口,该socket正常关闭或程序异常退出后的一段时间内,该端口依然维持原来的绑定状态,其他程序无法绑定该端口,如果设置了该选项这可以避免这个问题。实例如下:
int option_value = 1;
int length = sizeof(option_value);
setsockopt( sock_fd, SOL_SOCKET, SO_REUSEADDR, &option_value, length );
- **SO_TYPE**:用于获取套接字的类型,如**SOCK_DGRAM**、**SOCK_STREAM**、**SOCK_SEQPACKET**、**SOCK_RDM**等。该选项只能被函数getsockopt用来获取套接字类型,而不能使用函数setsockopt修改套接字的类型。
- **SO_ACCEPTCONN**:该选项用来检测套接字是否处于监听状态,如果为0表示处于非监听状态,如果为1表示正在监听,该选项只能被函数getsockopt用来获取监听状态信息。
- **SO_DONTROUTE**:设置该选项表示在发送IP数据报时不是用路由表来寻找路由
- **SO_BROADCAST**:该选项用来决定套接字是否能够在网络上广播数据。实际应用中要在网络上广播数据必须硬件支持广播(如以太网支持广播)并且使用的是**SOCK_DGRAM**套接字。系统默认不支持广播,如果希望该**SOCK_DGRAM**套接字支持广播,则可以这样来修改配置:
int option_value = 1;
setsockopt( sock_fd, SOL_SOCKET, SO_BROADCAST, &option_value, sizeof(int) );
- **SO_SNDBUF**和**SO_RCVBUF**:这两个选项用于设置套接字的发送和接收缓冲区的大小。对于TCP类型的套接字,缓冲区太小会影响TCP的流量控制;对于UDP类型的套接字,如果套接字的数据缓冲区满则后续数据将被丢弃,实际应用中根据需要设置一个合适的大小。
- **SO_ERROR**:获取套接字内部的错误变量so_error,当套接字上发生了异步错误时,系统将设置套接字的so\_error。异步错误是指错误的发生和错误被发现的时间不一致,通常在目的主机非正常关闭时发生这种错误。该选项只能被函数getsockopt用来获取so\_error。
注意:调用完函数getsockopt之后so_error的值将被自动重新初始化。
多路复用select()
- 在C/S模型中,服务器端需要同时处理多个客户端的连接请求,此时就需要使用多路复用。实现多路复用的最简单的方法是采用非阻塞方式套接字,服务器端不断地查询各个套接字的状态,如果有数据到达则读出数据,如果没有数据则查看下一个套接字。这种方法虽然简单,但在轮询的过程中浪费了大量的CPU时间,效率非常低。
- 另一种方法是服务器进程并不主动的询问套接字状态,而是向系统登记希望监视的套接字,然后阻塞。当套接字上有事件发生时(如有数据到达),系统通知服务器进程告知哪个套接字上发生了什么事件,服务器进程查询对应套接字并进行处理。在这种工作方式下,套接字上没有事件发生时,服务器进程不会去查询套接字的状态,从而不会浪费CPU的时间,提高了效率。
- 使用函数select可以实现第二种多路复用,在Shell下输入
man select
可获得函数原型:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数nfds是需要监视的文件描述符数,要见识的文件描述符值为0~n-1.参数readfds制定需要坚实的可读文件描述符集合,当这个集合中的一个描述符上有数据到达时,系统将通知调用select函数的程序。参数writefds指定需要监视的科协文件描述符集合,当这个集合中的某个描述符可以发送数据时,程序将受到通知。参数exceptfds指定需要监视的异常文件描述符集合,当该集合中的一个描述符发生异常时,程序将收到通知。参数timeout指定了阻塞的时间,如果在这段时间内监视的文件描述符上都没有事件发生,则函数select()将返回0。
- struct timeval的定义和前面的SO_RCVTIMEO和SO_SNDTIMEO两个选项相同。如果将timeout设为NULL,则函数select()将一直被阻塞,直到某个文件描述符上发生了事件,如果将timeout设为0,这此时相当于非阻塞方式,函数select()查询完文件描述符集合的状态后立即返回。如果将timeout设成某一时间值,在这个时间内如果没有事件发生,函数select()将返回;如果在这段时间内有事件发生,程序将收到通知。
注意:这里的文件描述符既可以是普通文件的描述符,也可以是套接字描述符。
- 系统为文件描述符提供了一系列的宏以方便操作:
void FD_CLR(int fd, fd_set *set); //将文件描述符fd从文件描述符集合set中删除
int FD_ISSET(int fd, fd_set *set); //测试fd是否在set中
void FD_SET(int fd, fd_set *set); //在文件描述符集合set中增加文件描述符fd
void FD_ZERO(fd_set *set); //将文件描述符集合set清空
- 如果select()设定的要监视的文件描述符集合中有描述符发生了事件,则select将返回发生事件的文件描述符的个数。下面是使用实例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
void display_time(const char* string)
{
int seconds;
seconds = time((time_t*) NULL);
printf("%s, %d\n", string, seconds);
}
int main(void)
{
fd_set readfds;
struct timeval timeout;
int ret;
//监视文件描述符0是否有数据输入,文件描述符0表示标准输入,即键盘输入
FD_ZERO(&readfds); //开始使用一个描述符集合前一般要将其清空
FD_SET(0, &readfds);
//设置阻塞时间为10秒
timeout.tv_sec = 10;
timeout.tv_usec = 0;
while(1)
{
display_time("before select");
ret = select(1, &readfds, NULL, NULL, &timeout);
display_time("after select");
switch(ret)
{
case 0:
printf("No data in 10s.\n");
exit(0);
break;
case -1:
perror("select");
exit(1);
break;
default:
getchar(); //将数据读入,否则标准输入上将一直为读就绪
printf("Data is available now.\n");
}
}
return 0;
}
- 程序说明:程序先初始化一个文件描述符集合readfds,然后将文件描述符0增加到这个文件描述符集合中,在调用select函数前将阻塞时间设置为10秒。函数time()用来获取从公元1970年1月1日0时0分0秒算起到现在所经过的秒数。执行结果如下:
lancibe@lancibe-TM1701:~/c$ ./select
before select, 1595038536
a
after select, 1595038537
Data is available now.
before select, 1595038537
after select, 1595038546
No data in 10s.
- 程序运行时,等待几秒按下键盘任意键和Enter键,或只按下Enter键。从执行过程及结果可以看出,在1595038537s按下了Enter键,select()立即返回并打印出提示信息。而后再次进入循环,在第1595038537s重新设置select()监视键盘动作,可以发现,这次虽然没有按下,但是select()在1595038546s返回了,也就是说这次阻塞的时间只有9s而不是预想中的10s。这是因为Linux系统会对select()的实现中修改参数timeout为剩余时间,我们第一次在阻塞一秒后按下键盘,剩余9秒,所以第二次就只阻塞5秒了。如果"case(0):"分支里面不调用exit(0),则从第三次开始将不再阻塞(因为timeout为0)而出现打印信息的刷屏。所以在使用函数select()对文件描述符集合进行监视时,一定要注意这个问题,如果是在循环中调用select(),则参数timeout的初始化必须放在循环内部。
一个面向连接的Client/Server实例
- 基于C/S模型的面向连接的网络程序,其基本流程如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iBc1qXL1-1595063603856)(/home/lancibe/桌面/面向连接的ClientServer模型框图.jpg “面向连接的Client/Server模型框图”)] - 下面使用TCP套接字来开发一个模拟用户远程登录的程序
服务器端程序的设计
- 服务器端的并发性:本程序采用多进程的方式来实现服务器对多个和客户端连接请求的响应。主程序创建套接字后将套接字绑定在4507端口,也可以绑定到其他端口。然后使套接字处于监听状态,调用accept函数等待来自客户端的连接请求。每接收一个新的客户端连接请求,服务器端进程就创建一个子进程,在子进程中处理该连接请求,服务器端进程继续等待来自其他客户端的连接请求。
- 数据格式:由于TCP是一种基于流的数据传输方式,数据没有固定的格式。因此需要在应用程序中定义一定的数据格式,本程序以回车符作为一次数据的结束标志
- 用户信息:本程序将用户信息保存在一个全局数组中。服务器段接收到来自客户端的登录用户名后,在该全局数组中查询是否存在该用户名。若不存在则回应字符’n’+结束标志(回车符’\n’);若存在,则回应字符’y’+结束标志,然后等待客户端的密码。若密码不匹配,则回应字符’n’+结束标志;弱密码匹配,则回应字符’y’+结束标志,然后发送一个欢迎登陆的字符串给客户端。
客户端程序的设计
- 客户端的应用程序相对于服务器端要简单,客户端主程序创建套接字后调用connect()连接服务器端的4507端口,使用从connect()返回的连接套接字与服务器端进行通信,交换数据。
代码部分
服务器端程序my_server.c
// C/S模型的服务器端
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <errno.h>
#include "my_recv.h"
#define SERV_PORT 4507 //服务器端的端口
#define LISTENQ 12 //连接请求队列的最大长度
#define INVALID_USERINFO 'n' //用户信息无效
#define VALID_USERINFO 'y' //用户信息有效
#define USERNAME 0 //接收到的是用户名
#define PASSWORD 1 //接收到的是密码
struct userinfo //保存用户名和密码的结构体
{
char username[32];
char password[32];
};
struct userinfo users[] = {
{"linux", "unix"},
{"4507", "4508"},
{"zyx", "zyx"},
{"test", "123456"},
{" ", " "} //以只含一个空格的字符串作为数组的结束标志
};
//查找用户名是否存在,存在返回该用户名的下标,不存在则返回-1,出错返回-2
int find_name(const char* name)
{
int i;
if(name == NULL)
{
printf("in find_name, NULL pointer");
return -2;
}
for (i = 0 ; users[i].username[0] != ' '; i++)
{
if(strcmp(users[i].username, name) == 0)
{
return i;
}
}
return -1;
}
//发送数据
void send_data(int conn_fd, const char* string)
{
if(send(conn_fd, string, strlen(string), 0) < 0 )
{
my_err("send", __LINE__); //my_err函数在my_recv.h中声明
}
}
int main()
{
int sock_fd, conn_fd;
int optval;
int flag_recv = USERNAME; //标识接收到的是用户名还是密码
int ret;
int name_num;
pid_t pid;
socklen_t cli_len;
struct sockaddr_in cli_addr,serv_addr;
char recv_buf[128];
// 创建一个TCP套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0)
{
my_err("socket", __LINE__);
}
//设置该套接字使之可以重新绑定端口
optval = 1;
if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&optval, sizeof(int)) < 0)
{
my_err("setsockopt", __LINE__);
}
//初始化服务器端地址结构
memset(&serv_addr, 0, sizeof (struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//将套接字绑定在本地端口
if(bind(sock_fd, (struct sockaddr*)&serv_addr , sizeof(struct sockaddr_in)) < 0)
{
my_err("bind", __LINE__);
}
//将套接字转化为监听套接字
if(listen (sock_fd, LISTENQ) < 0)
{
my_err("listen", __LINE__);
}
cli_len = sizeof (struct sockaddr_in);
while(1)
{
//通过accept接收客户端的连接请求,并返回连接套接字用于收发数据
conn_fd = accept(sock_fd, (struct sockaddr*)&cli_addr, &cli_len);
if(conn_fd < 0)
{
my_err("accept", __LINE__);
}
printf("accept a new client, ip:%s\n", inet_ntoa(cli_addr.sin_addr));
//创建一个子进程处理刚刚接受的连接请求
if ( (pid = fork()) == 0)
{
while(1)
{
if((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
{
perror("recv");
exit(1);
}
recv_buf[ret-1] = '\0'; //将数据结束标志'\n'换成字符串结束标志
if (flag_recv == USERNAME)//接收到的是用户名
{
name_num = find_name(recv_buf);
switch(name_num)
{
case -1:
send_data(conn_fd, "n\n");
break;
case -2:
exit(1);
break;
default:
send_data(conn_fd, "y\n");
flag_recv = PASSWORD;
break;
}
}
else if(flag_recv == PASSWORD)//接收到的是密码
{
if(strcmp(users[name_num].password, recv_buf) == 0)
{
send_data(conn_fd, "y\n");
send_data(conn_fd, "Welcome login my TCP server\n");
printf("%s login\n", users[name_num].username);
break; // 跳出while循环
}
else
send_data(conn_fd, "n\n");
}
}
close(sock_fd);
close(conn_fd);
exit(0);//结束子进程
}
else //父进程关闭刚刚接手的连接请求,执行accept等待其他连接请求
{
close(conn_fd);
}
}
return 0;
}
- 程序说明:本程序的结构与上图所示的服务器端一致,程序首先创建一个TCP套接字并将其绑定到本地端口上,然后将其转化为监听套接字,再调用函数accept接受来自客户端的连接请求,收到请求后创建一个子进程来单独处理该连接请求。在子进程中,验证客户端的登录用户名和密码,若正确者发送欢迎登陆信息。
自定义读取数据函数my_recv.c
#define MY_RECV_C
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "my_recv.h"
//自定义错误处理函数
void my_err(const char* err_string, int line)
{
fprintf(stderr, "line:%d ", line);
perror(err_string);
exit(1);
}
/* 函数名:my_recv
* 描 述:从套接字上读取一次数据(以'\n'为结束标志)
* 参 数:conn_fd ———— 从该连接套接字上接收数据
* data_buf ———— 读取到的数据保存在此缓冲中
* len ———— data_buf 所指向的空间长度
* 返回值:出错返回-1, 服务器端已关闭连接则返回0,成功返回读取的字节数
*/
int my_recv(int conn_fd, char* data_buf, int len)
{
static char recv_buf[BUFSIZE]; //自定义缓冲区,BUFSIZE定义在my_recv.h中
static char *pread; //指向下一次读取数据的位置
static int len_remain = 0; //自定义缓冲区中剩余字节数
int i;
//如果自定义缓冲区中没有数据,则从套接字读取数据
if (len_remain <= 0)
{
if((len_remain = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)
{
my_err("recv", __LINE__);
}
else if(len_remain == 0) //目的计算机端的socket连接关闭
{
return 0;
}
pread = recv_buf; //重新初始化pread指针
}
//从自定义缓冲区中读取一次数据
for (i = 0 ; *pread != '\n' ; i++)
{
if(i > len) //防止指针越界
{
return -1;
}
data_buf[i] == *pread++;
len_remain--;
}
//去除结束标志
len_remain--;
pread++;
return i; //读取成功
}
- 程序说明:这是一个封装了函数recv的自定义读取数据的函数,实际上是将套接字缓冲区中的数据拷贝到自定义缓冲区,然后再按格式(以’\n’为结束标志)读取出数据
自定义头文件my_recv.h
#ifndef __MY_RECV_H
#define __MY_RECV_H
#define BUFSIZE 1024
void my_err(const char* err_sting, int line);
int my_recv(int conn_fd, char* data_buf, int len);
#endif
客户端程序my_client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "my_recv.h"
#define INVALID_USERINFO 'n' //用户信息无效
#define VALID_USERINFO 'y' //用户信息有效
//获取用户输入存入到buf,buf的长度为len,用户输入数据以'\n'为结束标志
int get_userinfo(char* buf, int len)
{
int i;
int c;
if(buf == NULL)
return -1;
i = 0;
while(((c = getchar()) != '\n') && (c != EOF) && (i < len - 2))
{
buf[i++] = c;
}
buf[i++] = '\n';
buf[i++] = '\0';
return 0;
}
//输入用户名,然后通过fd发送出去
void input_userinfo(int conn_fd, const char* string)
{
char input_buf[32];
char recv_buf[BUFSIZE];
int flag_userinfo;
//输入用户信息知道正确为止
do
{
printf("%s:", string);
if(get_userinfo(input_buf, 32) < 0)
{
printf("error return from get_userinfo\n");
exit(1);
}
if(send(conn_fd, input_buf, strlen(input_buf), 0) < 0)
{
my_err("send", __LINE__);
}
//从连接套接字上读取一次数据
if(my_recv(conn_fd, recv_buf, sizeof(recv_buf)) < 0 )
{
printf("data is too long\n");
exit(1);
}
if(recv_buf[0] == VALID_USERINFO)
{
flag_userinfo = VALID_USERINFO;
}
else
{
printf("%s error, input again,\n",string);
flag_userinfo = INVALID_USERINFO;
}
}while(flag_userinfo == INVALID_USERINFO);
}
int main(int argc, char** argv)
{
int i;
int ret;
int conn_fd;
int serv_port;
struct sockaddr_in serv_addr;
char recv_buf[BUFSIZE];
//检查参数个数
if(argc != 5)
{
printf("Usage: [-p] [serv_port] [-a] [serv_address]\n");
exit(1);
}
//初始化服务器端地址结构
memset(&serv_addr, 0, sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
//从命令行获取服务器端的端口与地址
for(i = 1; i<argc ; i++)
{
if(strcmp("-p", argv[i]) == 0)
{
serv_port = atoi(argv[i+1]);
if(serv_port < 0 || serv_port > 65535)
{
printf("invalid serv_addr.sin_port\n");
exit(1);
}
else
{
serv_addr.sin_port = htons(serv_port);
}
continue;
}
if(strcmp("-a", argv[i]) == 0)
{
if(inet_aton(argv[i+1], &serv_addr.sin_addr) == 0)
{
printf("invalid server ip address\n");
exit(1);
}
continue;
}
}
//检测是否少输入了某项参数
if(serv_addr.sin_port == 0 || serv_addr.sin_addr.s_addr == 0)
{
printf("Usage: [-p] [serv_addr.sin_port] [-a] [serv_address]\n");
exit(1);
}
//创建一个TCP套接字
conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if(conn_fd < 0)
{
my_err("socket", __LINE__);
}
//向服务端发送链接请求
if(connect(conn_fd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr)) < 0)
{
my_err("connect", __LINE__);
}
//输入用户名和密码
input_userinfo(conn_fd, "username");
input_userinfo(conn_fd, "password");
//读取欢迎信息并打印出来
if((ret = my_recv(conn_fd, recv_buf, sizeof(recv_buf))) < 0)
{
printf("data is too long\n");
exit(1);
}
for(i = 0 ; i < ret ; i++)
{
printf("%c", recv_buf[i]);
}
printf("\n");
close(conn_fd);
return 0;
}
- 本程序的结构与前面所示的客户端一致,程序首先创建一个TCP套接字,然后调用函数connect请求与服务端连接。建立连接后,通过连接套接字首先发送用户名,然后等待服务器的确认,若用户名存在,者发送密码,若密码正确,则等待欢迎信息。