网络编程_网络编程基础
一:一个简单的TCP客户端、服务端
这里通过一个简单的tcp回射(抄的unpv,哈哈)服务程序和客户端程序来演示一下如何去使用系统API去编写网络程序。代码仅仅为了演示过程,中间省略了一些错误处理等过程。
首先我们来看一下TCP回射服务端程序:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int listenfd, connfd, n;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
n = read(sockfd, buf, MAXLINE); //暂且认为一次可以读上来,也没有被信号打断
if(n > 0)
{
write(sockfd, buf, n); //暂且认为全写完了,也不关心返回值
}
close(connfd);
}
}
从代码中我可能可以看到,服务端需要通过socket函数来创建一个socket (listenfd)(其实就是系统中的一个文件描述符),从系统中分配相应的资源;然后使用bind函数将该listenfd绑定到对应的端口和IP;listen接口的调用就让服务器进入监听状态,准备接受客户端的连接请求;接着调用accept等待客户端的连接,当有连接请求时,将连接(connfd)及连接的地址地址信息收上来;接下的的工作就是在connfd上调用read、write进行数据的收发了;数据收发完最后调用close函数关闭连接connfd,服务端再次既然到accept阶段。
接下来将介绍TCP客户端程序:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, stdin) != NULL) {
write(sockfd, sendline, strlen(sendline));
read(sockfd, recvline, MAXLINE);
fputs(recvline, stdout);
}
exit(0);
}
客户端程序也用到了服务端使用到的socket、write、read函数, 在创建好socket之后,和服务端要绑定端口、监听服务不同,客户端调用的是connect函数连接到服务端的指定ip和端口。
二:常用函数及数据结构介绍:
在上面TCP客户端、服务端程序中见到了网络编程中一些常用的API函数。这节我们分别简单介绍一下这些函数的用法及含义。
1:套接字地址结构:
目前实际开发过程中用到ipv6的还相对较少,这里只介绍ipv4的地址结构,想了解ipv6的同学自己可以找相关质料。
通用socket地址:
struct sockaddr
{
sa_family_t sa_family; //指代协议族类型,TCP套接字用 AF_INET
char sa_data[14]; //用于存放socket地址值,不同的协议族类型值和长度不一样。
};
ipv4地址:
struct in_addr
{
in_addr_t s_addr; //存储网络序的ipv4地址
};
struct sockaddr_in
{
sa_family_t sin_family; / /地址族, 只能为AF_INET
in_port_t sin_port; //网络序的端口号,无符号16字节整数
struct in_addr sin_addr; //存储ip地址,上面有介绍
char sin_zero[8]; //值为0,为了和上面sockaddr长度保存一致
};
2:套接字常用函数:
ip地址转换函数:
in_addr_t inet_addr(const char * strptr);
将点分十进制表示的ipv4地址转换为网络字节序整数表示的ipv4地址。
int inet_aton(const char *cp, struct in_addr *inp);
同inet_addr, 该函数通过参数inp返回网络字节序表示的ipv4地址,成功返回1,是吧返回0
char * inet_ntoa(struct in_addr in);
将网络字节序表示的ipv4地址转换为点分十进制表示的ipv4地址,该函数返回的地址是一个静态变量的指针,因此该函数是不可重入的。
socket相关函数:
int socket(int domain, int type, int protocol);
创建一个套接字接口描述符。成功返回套接字接口描述符,失败返回- 1。
int bind(int sockfd, struct sockaddr* myaddr, int addrlen);
服务端将一个套接字绑定到指定的ip和端口上(myaddr指定),如果没有指定ip则该套接字可以接受目标ip为本机任意一个ip的连接请求,否则只能接收目标ip为指定ip的连接请求,如果没有指定端口,那么内核会随即分配一个端口,不过基本上没有人会这样做。
int l isten(int sockfd, int backlog);
该函数会创建相关队列用于存放待处理的客户端连接,一般包含两个队列:已完成连接队列,未完成连接队列。调用该函数后服务端就开始接受客户端连接了。
Int accept(int sockfd, void *addr, int *addrlen);
服务端在调用listen函数后,调用该函数从已完成连接队列中返回一个已建立连接的套接字描述符(文件描述再此时才由操作系统创建哦),客户端地址及地址长度通过addr和addrlen参数返回。成功返回套接字描述符,失败返回-1。
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);
客户端在创建好套接字之后会调用该函数去连接到指定的ip和端口的TCP服务端,成功返回0,失败返回-1
int close(int fd);
关闭连接,同时关闭读和写,调用该函数会完成TCP四次分手的流程。在fork时系统会默认将父进程打开的socket引用计数加1,只用在socket的引用计数为0时才会真正关闭。
int shutdown( int fd, int howto );
该函数会根据howto参数的指定的取值来决定关闭socket的读、写、或读写而不关心socket的引用计数。Howto的取值可以为SHUT_RD (可读)、SHUT_WD (可写)、SHUT_RDWD (可读写,即完全关闭)。
size_t send(int fd, char *buf, int len, int flags );
size_t recv( int fd, char *buf, int len, int flags);
这两个函数是专门为socket的读写设计的, 在socket 上收取、写入buf中指定len长度数据。因为套接字描述符也是文件描述符,因此数据读写也可以调系统的读写函数read、write。
三:TCP服务端、客户端交互流程:
下图可以很好的表示出TCP服务端、客户端的流程及使用到的相关函数。这里就不做详细说明了。
参考文档
《UNIX 网络编程》卷一
《LINX高性能服务器编程》