您的位置:首页 > 编程语言

关于SOCKET编程

2016-01-18 16:37 288 查看
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

大小端的转换可以调用下面这些函数来进行:

#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);


socket address的数据结构如下所示:



<span style="white-space:pre">	</span>IPv4和IPv6的地址格式定义在 netinet/in.h 中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在 sys/un.h 中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UnixDomain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下。
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));


字符串转in_addr的函数:

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);


in_addr转字符串的函数:其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);


TCP通讯的流程如下所示:



首先是建立链接的过程:



服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

一般数据传输的过程为:建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

链接关闭的具体过程如下所示:



如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块:

#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
perror(s);
exit(1);

}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ( (n = accept(fd, sa, salenptr)) < 0 ) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");

}
return n;
}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (bind(fd, sa, salen) < 0)
perr_exit("bind error");

}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (connect(fd, sa, salen) < 0)
perr_exit("connect error");

}

void Listen(int fd, int backlog)
{
if (listen(fd, backlog) < 0)
perr_exit("listen error");

}

int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0 )
perr_exit("socket error");
return n;

}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1 ) {
if (errno == EINTR)
goto again;
else
return -1;

}
return n;

}mZ
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1 ) {
if (errno == EINTR)
goto again;
else
return -1;

}
return n;

}

void Close(int fd)
{
if (close(fd) == -1)
perr_exit("close error");

}

size_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char
*ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0 ) {
if (errno == EINTR)
nread = 0;
else
return -1;

} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;

}
return n - nleft;

}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0 ) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;

}
如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户端上传文件的协议,规定前12字节表示文件名,超过12字节的文件名截断,不足12字节的文件名用'\0'补齐,从第13字节开始是文件内容,上传完所有文件内容后关闭连接,服务器可以先调用readn读12个字节,根据文件名创建文件,然后在一个循环中调用read读文件内容并存盘,循环结束的条件是read返回0。字段长度固定的协议往往不够灵活,难以适应新的变化。常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行的比用'\0'的更常见,例如http协议。下面自行编写的readLine函数也很有用,可以用来处理可变长字符:

static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static * read_ptr;
static read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0 ) {
if (errno == EINTR)
goto again;
return -1;
}
else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;

}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1 ) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
}else
return -1;
}
*ptr = 0;
return n;
}


并发的接受请求, 可以使用并发处理多个client的请求,网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。

大体的代码框架如下所示:

listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
while (1) {
<span style="white-space:pre">	</span>connfd = accept(listenfd, ...);
<span style="white-space:pre">	</span>n = fork();
<span style="white-space:pre">	</span>if (n == -1) {
<span style="white-space:pre">		</span>perror("call to fork");
<span style="white-space:pre">		</span>exit(1);
<span style="white-space:pre">	</span>} else if (n == 0) {
<span style="white-space:pre">		</span>close(listenfd);
<span style="white-space:pre">		</span>while (1) {
<span style="white-space:pre">		</span>read(connfd, ...);
<span style="white-space:pre">		</span>...
<span style="white-space:pre">		</span>write(connfd, ...);
<span style="white-space:pre">	</span>}
<span style="white-space:pre">		</span>close(connfd);
<span style="white-space:pre">		</span>exit(0);
<span style="white-space:pre">	</span>} else
<span style="white-space:pre">	</span>close(connfd);
}


select的使用:

select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server。

下面是一个使用select的例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
socklen_t cliaddr_len;
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, 20);
maxfd = listenfd;
/* initialize */
maxi = -1;
/* index into client[]
* array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ;  ) {
rset = allset; /* structure assignment */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* savedescriptor */
break;
}
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset);
/* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i;
/* max index in* client[] array */
if (--nready == 0)
continue;
/* no more readable* descriptors */
for (i = 0; i <= maxi; i++) {
/* check all* clients for data */
if ( (sockfd = client[i]) < 0 )
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf,MAXLINE)) == 0 ) {
/* connection closed by* client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
int j;
for (j = 0; j < n; j++)
buf[j] =
toupper(buf[j]);
Write(sockfd, buf, n);
}
if (--nready == 0)
break; /* no more readable
descriptors */
}
}
}
}


UDP通讯的流程如下所示:



//server.c
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int sockfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);//DGRAM代表的是数据报
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr));
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (n == -1)
perr_exit("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str,sizeof(str)),ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
if (n == -1)
perr_exit("sendto error");
}
}

//client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
int sockfd, n;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
socklen_t servaddr_len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
while (fgets(buf, MAXLINE, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perr_exit("sendto error");
n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
if (n == -1)
perr_exit("recvfrom error");
Write(STDOUT_FILENO, buf, n);

}
Close(sockfd);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: