[1] I/O模型
进程操作文件时, 文件没有就绪(ready)
(
read--文件(设备或通讯)没有数据,
write--文件(设备或通讯)已满,
recv/recvfrom--套接字缓冲区空时(流式套接字),没有接收到数据包(报文套接字)
send/sendto--套接字缓冲区已满(流式套接字)
)时,操作系统会,采取如下策略实现系统调用:
1. block(阻塞)
让进程在上面的系统调用中,休眠等待。
2. noblock(非阻塞)
让进程立即返回错误,错误码如下:
EAGAIN 对于非socket文件
EWOULDBLOCK 对于socket文件
3. I/O多路复用
同时监控多个文件,如果都未就绪,就可以休眠等待,避免CPU利用率过高
[2] noblock
向文件的操作标志位中加入O_NONBLOCK,方法如下:
1. open
fd = open("test", ... | O_NONBLOCK);
2. fcntl
/*
* @brief 获取/设置文件标志
* @param[in] fd 文件描述符
* @param[in] cmd
* @li F_GETFL(void) 获取文件标志
* @li F_SETFL(int) 设置文件标志
* @return 失败返回-1 错误码见errno
* 成功返回:
* @li F_GETFL(void) 文件标志
* @li F_SETFL(int) 0
* @notes ... 不表示不定参数
*/
int fcntl(int fd, int cmd, ... /* arg */ );
例:
int oflags;
oflags = fcntl(sockfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, oflags);
[3] I/O多路复用
1. 头文件
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
2. 数据结构
typedef struct {
long int fds_bits[1024 / 32];
} fd_set;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
2. 函数
/*
* @brief 监控多个文件(是否可读/可写/异常)
* @param[in] nfds 最大的文件描述符 + 1
* @param[in | out] readfds 要监控是否可读的文件描述符集
* NULL 不监控文件的读
* @param[in | out] writefds 要监控是否可写的文件描述符集
* NULL 不监控文件的写
* @param[in | out] exceptfds 要监控是否异常的文件描述符集
* NULL 不监控文件的异常
* @param[in | out] timeout 超时值
* NULL 永远等待
* @return @li > 0 就绪的文件描述符个数
* @li = 0 超时
* @li -1 错误(见errno)
* @notes 非就绪的文件的文件描述符从文件描述符集中删除
* select会阻塞,直到有一个文件描述符就绪
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
* @brief 监控(查询)文件是否可读、可写和异常
* @param[in] nfds 最大的文件描述 + 1
* @param[in|out] readfds 是否可读的文件描述符
* @param[in|out] writefds 是否可读的文件描述符
* @param[in|out] exceptfds 是否可读的文件描述符
* @param[in|out] timeout 设置超时时间(NULL -- 永远阻塞), 输出超过就绪多长时间
* @return @li > 0 就绪的文件描述符个数
* @li 0 超时
* @li = -1 发生错误(错误码见errno)
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
/*
* @brief 设置文件描述符到文件描述符集
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_SET(int fd, fd_set *fds);
/*
* @brief 从文件描述符集中,清除文件描述符
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_CLR(int fd, fd_set *fds);
/*
* @brief 判断文件描述符集中的文件描述符是否置位
* @param[in] fd 文件描述符
* @param[in] fds 文件描述符集
* @return @li 1 文件描述符置位
* @li 0 文件描述符没有置位
*/
int FD_ISSET(int fd, fd_set *fds);
/*
* @brief 清空文件描述符集
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_ZERO(fd_set *fds);
[3] 服务器实现
1. UDP
轮训,并发不好实现
2. TCP
并发,编程简单
轮训, 容易交换数据,但是需要用到非阻塞 + IO多路复用
[4] 网络调试工具
1. telnet
测试服务端是否可连,命令如下:
$ telnet <IP> <端口号>
2. lsof
查看套接字信息:
$ lsof -i [udp | tcp] [:port]
$ lsof -i 列出所有的套接字信息
$ lsof -i tcp 列出使用tcp协议的套接字信息
$ lsof -i udp 列出使用tcp协议的套接字信息
$ lsof -i :<port> 列出绑定port端口的套接字信息
3. netstat
显示网络状态:
$ netstat -napt
$ netstat -napu
-n 显示ip地址代替机器名
-a 显示所有的套接字信息
-p 显示使用套接字的进程的ID和程序名
-t 显示使用tcp协议的套接字
-u 显示使用udp协议的套接字
4. tcpdump
linux系统下的抓包工具,常用命令形式:
$ sudo tcpdump -i eth0 -n 'port 23 and src host 192.168.1.100' -w log.pcap
sudo root权限运行
-i eth0 指定抓哪个网络接口上的包
-n 源地址和目标地址显示ip地址,而不是主机名
'port 23 and src host 192.168.1.100' 监控端口为23,并且源主机地址为192.168.1.100的包
-w log.pcap 详细协议内容记录到log.pcap,该文wireshark可以直接打开
5. wireshark
见《wireshark抓包教程.doc》
[5] 网络协议分析
TCP协议: 分组
IP协议: 分片
分组和分片的目的是,数据链路层的包不会大于网络的MTU
进程操作文件时, 文件没有就绪(ready)
(
read--文件(设备或通讯)没有数据,
write--文件(设备或通讯)已满,
recv/recvfrom--套接字缓冲区空时(流式套接字),没有接收到数据包(报文套接字)
send/sendto--套接字缓冲区已满(流式套接字)
)时,操作系统会,采取如下策略实现系统调用:
1. block(阻塞)
让进程在上面的系统调用中,休眠等待。
2. noblock(非阻塞)
让进程立即返回错误,错误码如下:
EAGAIN 对于非socket文件
EWOULDBLOCK 对于socket文件
3. I/O多路复用
同时监控多个文件,如果都未就绪,就可以休眠等待,避免CPU利用率过高
#ifndef __TFTP_H__
#define __TFTP_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LEN_PACKET_LEN 10
#define LEN_PACKET_FUNC 1
#define LEN_PACKET_HEAD (LEN_PACKET_LEN + LEN_PACKET_FUNC)
#define LEN_FILE_FLAGS 1
#define LEN_FILE_NAME 20
enum {
FUNC_START = 0,
FUNC_LIST,
FUNC_GET,
FUNC_PUT,
FUNC_END
};
int recv_fix_len(int sockfd, char *buf, int len);
int send_fix_len(int sockfd, const char *buf, int len);
void packet_set_len(char *packet, int len);
int packet_get_len(const char *packet);
void packet_set_func(char *packet, int func);
int packet_get_func(const char *packet);
int packet_get_func(const char *packet);
int packet_recv_proc(int sockfd);
int client_exe_list(int sockfd);
int server_exe_list(int sockfd);
#endif // __TFTP_H__
/*
* 实现目标:
* tftp协议
*
* 实现步骤:
* 1. 制定协议
* 客户端--->服务端
* +----------------+------------+--------------------------+-------+
* | 包体长度(10B) | 功能号(1B) | 包体 | 解释 |
* +----------------+------------+--------------------------+-------+
* | | '1' | 无 | list |
* +----------------+------------+--------------------------+-------+
* | | '2' | 文件名 | get |
* +----------------+------------+--------+-----------------+-------+
* | | '3' | 文件名 | 文件内容 | put |
* +----------------+------------+--------+-----------------+-------+
*
* 服务端--->客户端
* +----------------+------------+--------------------------+-------+
* | 包体长度(10B) | 功能号(1B) | 包体 | 解释 |
* +----------------+------------+--------------------------+-------+
* | | '1' | 文件列表 | list |
* +----------------+------------+----------+---------------+-------+
* | | '2' | 文件状态 | 文件内容 | get |
* +----------------+------------+----------+---------------+-------+
* | | '3' | 文件接收标志 | put |
* +----------------+------------+--------+-----------------+-------+
* 1. 包体长度
* 包体长度,用十进制字符串表示,比如: get数据包中的文件名是100字节 ---> "0000000100" --> 100
*
* 2. 文件名
* 20个字节的字符串
*
* 3. 文件状态
* '0' - 文件不存在
* '1' - 文件存在
*
* 4. 文件接收标志
* '0' - 接收成功
* '1' - 失败
*
* 2. 包头处理
* 2.1 打包/解包包体长度
* 2.2 打包/解包功能号
* 2.3 接收并解析包头
*
* 3. 协议解析框架
*
* 4. list
*
* 5. get
* 6. put
*
*/
#include "tftp.h"
int recv_fix_len(int sockfd, char *buf, int len)
{
int ret = 0;
int recv_len = 0;
while (recv_len < len){
ret = recv(sockfd, buf + recv_len, len - recv_len, 0);
if (-1 == ret){
perror("Fail to recv");
return -1;
}
recv_len += ret;
}
return 0;
}
int send_fix_len(int sockfd, const char *buf, int len)
{
int ret = 0;
int send_len = 0;
while (send_len < len){
ret = send(sockfd, buf + send_len, len - send_len, 0);
if (-1 == ret){
perror("Fail to send");
return -1;
}
send_len += ret;
}
return 0;
}
// 2.1 打包/解包包体长度
void packet_set_len(char *packet, int len)
{
char buf[LEN_PACKET_LEN + 1];
char fmt[32];
// fmt = %010d
// sprintf(fmt, %010d, ...)
sprintf(fmt, "%%0%dd", LEN_PACKET_LEN);
// sprintf(buf, "%100d", 100);
// buf = "00000000100"
sprintf(buf, fmt, len);
memcpy(packet, buf, LEN_PACKET_LEN);
}
int packet_get_len(const char *packet)
{
char buf[LEN_PACKET_LEN + 1];
memcpy(buf, packet, LEN_PACKET_LEN);
buf[LEN_PACKET_LEN] = '\0';
return atoi(buf);
}
// 2.2 打包/解包功能号
void packet_set_func(char *packet, int func)
{
char buf[LEN_PACKET_FUNC + 1];
char fmt[32];
sprintf(fmt, "%%0%dd", LEN_PACKET_FUNC);
sprintf(buf, fmt, func);
memcpy(packet + LEN_PACKET_LEN, buf, LEN_PACKET_FUNC);
}
int packet_get_func(const char *packet)
{
char buf[LEN_PACKET_FUNC + 1];
memcpy(buf, packet + LEN_PACKET_LEN, LEN_PACKET_FUNC);
buf[LEN_PACKET_FUNC] = '\0';
return atoi(buf);
}
// 2.3 接收并解析包头
int packet_recv_head(int sockfd, int *func)
{
int len = 0;
char packet_head[LEN_PACKET_LEN + LEN_PACKET_FUNC];
// 接收包头
len = recv_fix_len(sockfd, packet_head, sizeof(packet_head));
if (-1 == len){
return -1;
}
// 解析包头
len = packet_get_len(packet_head);
*func = packet_get_func(packet_head);
if (*func <= FUNC_START || *func >= FUNC_END){
return -1;
}
return len;
}
// 3. 协议解析框架
int packet_recv_proc(int sockfd)
{
int ret = 0;
int len;
int func;
len = packet_recv_head(sockfd, &func);
if (-1 == len){
return -1;
}
printf("--------------------------------------------\n");
printf("recv: func = %d, len = %d\n", func, len);
printf("--------------------------------------------\n");
switch (func){
case FUNC_LIST:
ret = server_exe_list(sockfd);
break;
case FUNC_GET:
ret = server_exe_get(sockfd, len);
break;
case FUNC_PUT:
break;
default:
break;
}
return 0;
}
// 4. list
int client_exe_list(int sockfd)
{
int ret = 0;
int len;
int func;
char packet[1024];
// 打包发送list协议
packet_set_len(packet, 0);
packet_set_func(packet, FUNC_LIST);
ret = send_fix_len(sockfd, packet, LEN_PACKET_HEAD);
if (-1 == ret){
return -1;
}
// 接收服务的返回
len = packet_recv_head(sockfd, &func);
if (-1 == len){
return -1;
}
// 接收文件列表
ret = recv_fix_len(sockfd, packet, len);
if (-1 == ret){
return -1;
}
printf("%s\n", packet);
return 0;
}
int server_exe_list(int sockfd)
{
int ret = 0;
int len = LEN_PACKET_HEAD;
char packet[1024];
DIR *pdir;
struct dirent *pdirent;
// 获取文件列表
pdir = opendir(".");
if (NULL == pdir){
perror("Fail to opendir");
return -1;
}
// 获取目录下的文件
while ((pdirent = readdir(pdir)) != NULL) {
// 隐藏文件
if (pdirent->d_name[0] == '.'){
continue;
}
// tftp.c tftp.h
// "tftp.c tftp.h "
sprintf(packet + len, "%s ", pdirent->d_name);
len += strlen(packet + len);
}
// 设置包头
packet_set_len(packet, len - LEN_PACKET_HEAD);
packet_set_func(packet, FUNC_LIST);
// 发送
ret = send_fix_len(sockfd, packet, len);
return ret;
}
// get命令客户端
int client_exe_get(int sockfd, const char *name)
{
int ret = 0;
int file_len = 0;
int recv_len = 0;
int fd;
char packet[1024];
// 打包发送get命令请求
packet_set_len(packet, strlen(name));
packet_set_func(packet, FUNC_GET);
memcpy(packet + LEN_PACKET_HEAD, name, strlen(name));
ret = send_fix_len(sockfd, packet, LEN_PACKET_HEAD + strlen(name));
if (-1 == ret){
return -1;
}
// 接收解析服务器的返回包
ret = recv_fix_len(sockfd, packet, LEN_PACKET_HEAD + LEN_FILE_FLAGS);
if (-1 == ret){
return -1;
}
if (packet[LEN_PACKET_HEAD] != '0'){
// 失败
printf("Get file is not exist.\n");
return 0;
}
// 成功
file_len = packet_get_len(packet) - LEN_FILE_FLAGS;
// 接收保存文件的内容
// 创建并打开文件
fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (-1 == fd){
perror("Fail to open.");
return -1;
}
// 接收并写入文件
recv_len = 0;
while (recv_len < file_len){
ret = recv(sockfd, packet, sizeof(packet));
if (-1 == ret){
break;
}
write(fd, packet, ret);
recv_len += ret;
}
close(fd);
printf("recv %s successfully.\n", name);
}
// get服务端
// len 包体长度
int server_exe_get(int sockfd, int len)
{
int ret = 0;
int fd;
int file_flag = 0;
int file_len;
int send_len;
char name[LEN_FILE_NAME + 1];
char packet[1024];
char buf[32];
// 接收文件名
ret = recv_fix_len(sockfd, name, len);
if (-1 == ret){
return -1;
}
name[len] = '\0';
// 打开文件
fd = open(name, O_RDONLY);
if (-1 == fd){
perror("Fail to open.");
file_flag = 1;
}
// 读取文件长度
file_len = lseek(fd, 0, SEEK_END);
// 打包发送包头及文件标志
packet_set_len(packet, file_len + LEN_FILE_FLAGS);
packet_set_func(packet, FUNC_GET);
// 支持扩展
sprintf(buf, "%d", file_flag);
memcpy(packet + LEN_PACKET_HEAD, buf, LEN_FILE_FLAGS);
// 不支持扩展
// packet[LEN_PACKET_HEAD] = file_flag + '0';
ret = send_fix_len(sockfd, packet, LEN_PACKET_HEAD + LEN_FILE_FLAGS);
if (-1 == ret){
return -1;
}
// 打包发送文件内容
lseek(fd, 0, SEEK_SET);
send_len = 0;
while (send_len < file_len){
ret = read(fd, packet, sizeof(packet));
if (-1 == ret){
perror("Fail to read.");
return -1;
}
send_len += ret;
ret = send_fix_len(sockfd, packet, ret);
if (-1 == ret){
return -1;
}
}
printf("send successfully.\n");
}
/*
* 实现目标:
* 文件服务器
* list
* get
* put
*
* 实现步骤:
* 1. 多个客户端并发响应
* 1.1 线程回调函数
* 1.2 创建线程
*
* 2. tftp协议
* 见《tftp.c》
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
// bzero
#include <strings.h>
// net
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tftp.h"
// 1.1 线程回调函数
void *do_client(void *arg)
{
int clientfd = (int)(long int)arg;
packet_recv_proc(clientfd);
close(clientfd);
return NULL;
}
// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
int sockfd;
int clientfd;
int ret;
pthread_t tid;
struct sockaddr_in server_addr;
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(peer_addr);
if (argc < 3){
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd){
perror("Fail to socket.");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret){
perror("Fail to bind.");
exit(EXIT_FAILURE);
}
ret = listen(sockfd, 10);
if (-1 == ret){
perror("Fail to listen.");
exit(EXIT_FAILURE);
}
while (1){
clientfd = accept(sockfd, (struct sockaddr *)&peer_addr, &addrlen);
if (-1 == clientfd){
perror("Fail to accept.");
break;
}
printf("------------------------------------------\n");
printf("ip : %s\n", inet_ntoa(peer_addr.sin_addr));
printf("port : %d\n", ntohs(peer_addr.sin_port));
printf("------------------------------------------\n");
// 1.2 创建线程
ret = pthread_create(&tid, NULL, do_client, (void *)(long int)clientfd);
if (ret < 0){
fprintf(stderr, "Fail to pthread_create: %d\n", ret);
close(clientfd);
break;
}
// 设置为分离线程
pthread_detach(tid);
}
close(sockfd);
return 0;
}
[2] noblock
向文件的操作标志位中加入O_NONBLOCK,方法如下:
1. open
fd = open("test", ... | O_NONBLOCK);
2. fcntl
/*
* @brief 获取/设置文件标志
* @param[in] fd 文件描述符
* @param[in] cmd
* @li F_GETFL(void) 获取文件标志
* @li F_SETFL(int) 设置文件标志
* @return 失败返回-1 错误码见errno
* 成功返回:
* @li F_GETFL(void) 文件标志
* @li F_SETFL(int) 0
* @notes ... 不表示不定参数
*/
int fcntl(int fd, int cmd, ... /* arg */ );
例:
int oflags;
oflags = fcntl(sockfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, oflags);
[3] I/O多路复用
1. 头文件
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
2. 数据结构
typedef struct {
long int fds_bits[1024 / 32];
} fd_set;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
2. 函数
/*
* @brief 监控多个文件(是否可读/可写/异常)
* @param[in] nfds 最大的文件描述符 + 1
* @param[in | out] readfds 要监控是否可读的文件描述符集
* NULL 不监控文件的读
* @param[in | out] writefds 要监控是否可写的文件描述符集
* NULL 不监控文件的写
* @param[in | out] exceptfds 要监控是否异常的文件描述符集
* NULL 不监控文件的异常
* @param[in | out] timeout 超时值
* NULL 永远等待
* @return @li > 0 就绪的文件描述符个数
* @li = 0 超时
* @li -1 错误(见errno)
* @notes 非就绪的文件的文件描述符从文件描述符集中删除
* select会阻塞,直到有一个文件描述符就绪
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
* @brief 监控(查询)文件是否可读、可写和异常
* @param[in] nfds 最大的文件描述 + 1
* @param[in|out] readfds 是否可读的文件描述符
* @param[in|out] writefds 是否可读的文件描述符
* @param[in|out] exceptfds 是否可读的文件描述符
* @param[in|out] timeout 设置超时时间(NULL -- 永远阻塞), 输出超过就绪多长时间
* @return @li > 0 就绪的文件描述符个数
* @li 0 超时
* @li = -1 发生错误(错误码见errno)
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
/*
* @brief 设置文件描述符到文件描述符集
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_SET(int fd, fd_set *fds);
/*
* @brief 从文件描述符集中,清除文件描述符
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_CLR(int fd, fd_set *fds);
/*
* @brief 判断文件描述符集中的文件描述符是否置位
* @param[in] fd 文件描述符
* @param[in] fds 文件描述符集
* @return @li 1 文件描述符置位
* @li 0 文件描述符没有置位
*/
int FD_ISSET(int fd, fd_set *fds);
/*
* @brief 清空文件描述符集
* @param[in] fd 文件描述符
* @param[out] fds 文件描述符集
*/
void FD_ZERO(fd_set *fds);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
int ret;
int sockfd;
struct sockaddr_in server_addr;
char packet[1024];
if (argc < 3){
fprintf(stderr, "Usage: %s <server ip> <server port>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd){
perror("Fail to socket.");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret){
perror("Fail to connect.");
close(sockfd);
exit(EXIT_FAILURE);
}
while (1){
putchar('>');
fgets(packet, sizeof(packet), stdin);
packet[strlen(packet) - 1] = '\0';
ret = send(sockfd, packet, strlen(packet), 0);
if (-1 == ret){
perror("Fail to send.");
close(sockfd);
break;
}
if (strcmp(packet, "bye") == 0) break;
}
close(sockfd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
int ret;
int sockfd;
int clientfd = -1;
struct sockaddr_in server_addr;
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(peer_addr);
char packet[1024];
int oflags;
if (argc < 3){
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd){
perror("Fail to socket.");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret){
perror("Fail to bind.");
exit(EXIT_FAILURE);
}
ret = listen(sockfd, 10);
if (-1 == ret){
perror("Fail to listen.");
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞
oflags = fcntl(sockfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, oflags);
while (1) {
ret = accept(sockfd, (struct sockaddr *)&peer_addr, &addrlen);
if (-1 == ret && (errno != EAGAIN) && (errno != EWOULDBLOCK)){
perror("Fail to accept.");
break;
} else if (ret >= 0) {
clientfd = ret;
// 设置套接字为非阻塞
oflags = fcntl(clientfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(clientfd, F_SETFL, oflags);
printf("---------------------------------------\n");
printf("ip : %s\n", inet_ntoa(peer_addr.sin_addr));
printf("port : %d\n", ntohs(peer_addr.sin_port));
printf("---------------------------------------\n");
}
if (-1 == clientfd) continue;
ret = recv(clientfd, packet, sizeof(packet), 0);
if (-1 == ret && (errno != EAGAIN) && (errno != EWOULDBLOCK)){
perror("Fail to recv.");
close(clientfd);
break;
}
packet[ret] = '\0';
if (ret > 0){
printf("------------------------\n");
printf("recv : %s\n", packet);
printf("------------------------\n");
}
if (strcmp(packet, "bye") == 0) break;
}
close(clientfd);
close(sockfd);
return 0;
}
[3] 服务器实现
1. UDP
轮训,并发不好实现
2. TCP
并发,编程简单
轮训, 容易交换数据,但是需要用到非阻塞 + IO多路复用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
int ret;
int sockfd;
struct sockaddr_in server_addr;
char packet[1024];
if (argc < 3){
fprintf(stderr, "Usage: %s <server ip> <server port>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd){
perror("Fail to socket.");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret){
perror("Fail to connect.");
close(sockfd);
exit(EXIT_FAILURE);
}
while (1){
putchar('>');
fgets(packet, sizeof(packet), stdin);
packet[strlen(packet) - 1] = '\0';
ret = send(sockfd, packet, strlen(packet), 0);
if (-1 == ret){
perror("Fail to send.");
close(sockfd);
break;
}
if (strcmp(packet, "bye") == 0) break;
}
close(sockfd);
return 0;
}
/*
* 实现目标:
* 使用轮训实现TCP服务器
*
* 实现步骤:
* 1. 直接使用阻塞套接字(不行)
* 2. 修改为非阻塞套接字(一个客户端套接字和监听套接字) CPU利用率太高,一直在主循环中执行,即使所以客户端及监听套接字空闲
* 3. I/O多路复用(同时监听多个文件,只要有一个就绪,处理改文件,所有非就绪,可以休眠)
* select
* 3.1 创建文件描述集
* 3.2 添加所有有监控的文件描述符到文件描述符集
* 3.3 利用select监控文件是否就绪
* 对就绪的文件 处理
* 没有就绪的文件 休眠
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
int ret;
int sockfd;
int clientfd = -1;
struct sockaddr_in server_addr;
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(peer_addr);
char packet[1024];
int oflags;
int maxfd;
int i;
struct timeval timeout = {10, 0};
// 3.1 创建文件描述集
fd_set readfds;
fd_set bak_readfds;
FD_ZERO(&readfds);
FD_ZERO(&bak_readfds);
if (argc < 3){
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd){
perror("Fail to socket.");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (-1 == ret){
perror("Fail to bind.");
exit(EXIT_FAILURE);
}
ret = listen(sockfd, 10);
if (-1 == ret){
perror("Fail to listen.");
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞
oflags = fcntl(sockfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, oflags);
// 3.2 添加监听的文件描述符到文件描述符集
FD_SET(sockfd, &bak_readfds);
maxfd = sockfd;
while (1) {
// 定时10S
timeout.tv_sec = 10;
timeout.tv_usec = 0;
readfds = bak_readfds;
ret = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (-1 == ret){
break;
} else if (0 == ret) {
printf("select timeout\n");
continue;
}
for (i = 0; i <= maxfd; i++){
if (!FD_ISSET(i, &readfds)) {
continue;
}
if (i == sockfd) {
ret = accept(sockfd, (struct sockaddr *)&peer_addr, &addrlen);
if (-1 == ret && (errno != EAGAIN) && (errno != EWOULDBLOCK)){
perror("Fail to accept.");
break;
} else if (ret >= 0) {
clientfd = ret;
// 设置套接字为非阻塞
oflags = fcntl(clientfd, F_GETFL);
oflags |= O_NONBLOCK;
fcntl(clientfd, F_SETFL, oflags);
// 添加客户端文件描述符
FD_SET(clientfd, &bak_readfds);
if (maxfd < clientfd) {
maxfd = clientfd;
}
printf("---------------------------------------\n");
printf("ip : %s\n", inet_ntoa(peer_addr.sin_addr));
printf("port : %d\n", ntohs(peer_addr.sin_port));
printf("---------------------------------------\n");
}
continue;
}
ret = recv(i, packet, sizeof(packet), 0);
if (-1 == ret && (errno != EAGAIN) && (errno != EWOULDBLOCK)){
perror("Fail to recv.");
close(clientfd);
break;
}
packet[ret] = '\0';
if (ret > 0){
printf("------------------------\n");
printf("recv : %s\n", packet);
printf("------------------------\n");
}
}
if (strcmp(packet, "bye") == 0) break;
}
close(clientfd);
close(sockfd);
return 0;
}
[4] 网络调试工具
1. telnet
测试服务端是否可连,命令如下:
$ telnet <IP> <端口号>
2. lsof
查看套接字信息:
$ lsof -i [udp | tcp] [:port]
$ lsof -i 列出所有的套接字信息
$ lsof -i tcp 列出使用tcp协议的套接字信息
$ lsof -i udp 列出使用tcp协议的套接字信息
$ lsof -i :<port> 列出绑定port端口的套接字信息
3. netstat
显示网络状态:
$ netstat -napt
$ netstat -napu
-n 显示ip地址代替机器名
-a 显示所有的套接字信息
-p 显示使用套接字的进程的ID和程序名
-t 显示使用tcp协议的套接字
-u 显示使用udp协议的套接字
4. tcpdump
linux系统下的抓包工具,常用命令形式:
$ sudo tcpdump -i eth0 -n 'port 23 and src host 192.168.1.100' -w log.pcap
sudo root权限运行
-i eth0 指定抓哪个网络接口上的包
-n 源地址和目标地址显示ip地址,而不是主机名
'port 23 and src host 192.168.1.100' 监控端口为23,并且源主机地址为192.168.1.100的包
-w log.pcap 详细协议内容记录到log.pcap,该文wireshark可以直接打开
5. wireshark
见《wireshark抓包教程.doc》
[5] 网络协议分析
TCP协议: 分组
IP协议: 分片
分组和分片的目的是,数据链路层的包不会大于网络的MTU