因为需要开始学习网络编程,因为之前c,c++比较熟练,所以挑选了容易上手的《TCP/IP网络编程》这本书。
网络编程中接受连接请求的套接字创建过程可整理如下:
#include <sys/socket.h>//用到的头文件
1. 调用socket函数创建套接字
int socket(int domain, int type, int protocol);
成功时返回文件描述,失败时返回-1。
2. 调用bind函数分配IP地址和端口号
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
成功时返回0,失败时返回-1。
3. 调用listen函数转为可接受请求状态
int listen(int sockfd, int backlog);
成功时返回0,失败时返回-1。
4. 调用accept函数受理连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
成功时返回文件描述,失败时返回-1。
请求连接函数:
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
成功时返回0,失败时返回-1。
编写“Hello world!”服务器端
hello_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
编写客户端 hello_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
str_len = read(sock, message, sizeof(message) - 1);
if(str_len == -1)
error_handling("read() error!");
printf("Message from server : %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
上述两个示例应在Linux环境下编译运行。
gcc hello_server.c -o hserver
./hserver 9190
gcc hello_client.c -o hclient
./hclient 127.0.0.1 9190
可以看到:Message from server : Hello World!
在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。Windows则与Linux不同,是要区分socket和文件的。因此在windows中需要调用特殊的数据传输相关函数。
Linux下,文件描述符是系统分配给文件或套接字的整数。实际上,学习C语言过程中用过的标准输入输出及标准错误在Linux中也被分配表1-1中的文件描述:
文件描述符 | 对象 |
---|---|
0 | 标准输入:Standard Input |
1 | 标准输出:Standard Output |
2 | 标准错误:Standard Error |
文件描述符有时也称为文件句柄,但“句柄”主要是Windows中的术语。因此,之后如果涉及Windows平台将使用“句柄”,如果是Linux平台则用“描述符”。
打开文件
int open(const char *path, int flag);
第一个参数(path)是打开的目标文件名及路径信息
第二个参数(flag)是文件打开模式(文件特性信息)
成功时返回文件描述,失败时返回-1
表1-2是此函数第二个参数flag可能的常量值及含义。如需传递多个参数,则应通过位或运算(OR)符组合并传递。
打开模式 | 含义 |
---|---|
O_CREAT | 必要时创建文件 |
O_TRUNC | 删除全部现有数据 |
O_APPEND | 维持现有数据,保存到其后面 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
关闭文件
int close(int fd);
fd:需要关闭的文件或套接字的文件描述符
成功时返回0,失败时返回-1。
将数据写入文件
ssize_t write(int fd, const void* buf, size_t nbytes);
fd:显示数据传输对象的文件描述符
buf:保存要传输数据的缓冲地址值
nbytes:要传输数据的字节数
成功时返回写入的字节数,失败时返回-1。
此函数定义中,size_t是通过typedef声明的unsigned int类型。对ssize_t来说,size_t前面的s代表signed,即ssize_t是通过typedef声明的signed int类型。
为了与程序员定义的新数据类型加以区分,操作系统定义的数据类型会添加后缀_t。
示例:low_open.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char *message);
int main(void)
{
int fd;
char buf[] = "Let's go\n";
fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC);
if(fd == -1)
error_handling("open() error");
printf("file descriptor: %d \n",fd);
if(write(fd,buf,sizeof(buf)) == -1)
error_handling("write() error");
close(fd);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
读取文件中的数据
ssize_t read(int fd, const void* buf, size_t nbytes);
fd:显示数据接受对象的文件描述符
buf:要保存接收数据的缓冲地址值
nbytes:要接收数据的最大字节数
成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1。
示例:low_read.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char *message);
int main(void)
{
int fd;
char buf[BUFSIZ];
fd = open("data.txt", O_RDONLY);
if(fd == -1)
error_handling("open() error");
printf("file descriptor: %d \n",fd);
if(read(fd,buf,sizeof(buf)) == -1)
error_handling("read() error");
printf("file data: %s",buf);
close(fd);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
在进行winsock编程时,首先必须调用WSAStartup函数,设置程序中用的Winsock版本,并初始化相应版本库。
#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested:程序员要用的Winsock版本信息
lpWSAData WSADATA结构体变量的地址值
成功时返回0,失败时返回非零的代码值
- Winsock中存在多个版本,应准备WORD类型的(WORD是通过typedef生命定义的unsingned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested。如版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。
我们可以借助MAKEWORD宏函数来构建WORD型版本信息。
例如1.2我们可以用MAKEWORD(1, 2); - LPWSADATA是WSADATA的指针类型
示例:这段代码几乎已经成为Winsock编程的公式
#include <winsock2.h>
int main(int argc,char* argv[])
{
WSADATA wsaData;
……
if(WSAStartup(MAKEWORD(2,2),&wsaData) != 0)
ErrorHandling("WSAStartup() error!");
……
return 0;
}
基于Windows的套接字相关函数示例
SOCKET socket(int af, int type, int protocol);
成功时返回套接字句柄,失败时返回INVALID_SOCKET
调用其分配IP地址和端口号
int bind(SOCKET s, const struct sockaddr * name, int namelen);
成功时返回0,失败时返回SOCKET_ERROR。
调用其使套接字可接收客户端连接
int listen(SOCKET s, int backlog);
成功时返回0,失败时返回SOCKET_ERROR。
调用其受理客户端连接请求
SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen);
成功时返回套接字句柄,失败时返回INVALID_SOCKET
调用其从客户端发送连接请求
int connect(SOCKET s, const struct sockaddr * name, int namelen);
成功时返回0,失败时返回SOCKET_ERROR。
最后这个函数在关闭套接字时调用。Linux中,关闭文件和套接字时都会调用close函数;
而windows中有专门用来关闭套接字的函数
int closesocket(SOCKET s);
成功时返回0,失败时返回SOCKET_ERROR。
Windows中的句柄相当于Linux中的文件描述符。只不过Windows中要区分文件句柄和套接字句柄。虽然都称为句柄,但不像Linux那样完全一致。文件句柄相关函数与套接字句柄相关函数是有区别的,这一点不同于Linux文件描述符。
hello_server.c(win)
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;/*不同*/
SOCKET serv_sock,clnt_sock;/*不同*/
SOCKADDR_IN serv_addr;/*不同*/
SOCKADDR_IN clnt_addr;/*不同*/
int clnt_addr_size;
char message[] = "Hello World!";
if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData))/*不同*/
error_handling("WSAStartup() error!");
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == INVALID_SOCKET)/*不同*/
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (SOCKADDR*) &serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)/*不同*/
error_handling("bind() error");
if(listen(serv_sock, 5) == SOCKET_ERROR)/*不同*/
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (SOCKADDR*)&clnt_addr,&clnt_addr_size);
if(clnt_sock == INVALID_SOCKET)/*不同*/
error_handling("accept() error");
send(clnt_sock, message, sizeof(message),0);/*不同*/
closesocket(clnt_sock);/*不同*/
closesocket(serv_sock);/*不同*/
WSACleanup();/*不同*/
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
hello_client.c(win)
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;//不同
SOCKET sock;//不同
SOCKADDR_IN serv_addr;//不同
char message[30];
int str_len;
if(argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2),&wsaData))//不同
error_handling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET)//不同
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)//不同
error_handling("connect() error!");
str_len = recv(sock, message, sizeof(message) - 1,0);//不同
if(str_len == -1)
error_handling("read() error!");
printf("Message from server : %s \n", message);
closesocket(sock);//不同
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
运行就是windows下那一套
基于windows的I/O函数
Windows严格区分文件I/O函数和套接字I/O函数。下面介绍Winsock数据传输函数
int send(SOCKET s, const char * buf, int len, int flags);
s:表示数据传输对象连接的套接字句柄值
buf:保存待传输数据的缓冲地址值
len:要传输的字节数
flags:传输数据时用到的多种选项信息
成功时返回传输字节数,失败时返回SOCKET_ERROR。
此函数与Linux的write函数相比,知识多出了最后的flags参数。传递0表示不设置任何选项。
但需要注意的是,send函数并非windows中独有,Linux中也有同样的函数,它也来自于BSD套接字。前面Linux中使用read、write函数,是为了强调Linux环境下文件I/O和套接字I/O相同。
int recv(SOCKET s, const char * buf, int len, int flags);
s:表示数据接收对象连接的套接字句柄值
buf:保存接收数据的缓冲地址值
len:能够接收的最大字节数
flags:接收数据时用到的多种选项信息
成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKET_ERROR。
最后再记录下补缺的知识点
C语言规定main函数的参数只能有两个,习惯上这两个参数写为argc和argv。因此,main函数的函数头可写为: main (argc,argv)
C语言还规定argc(第一个形参)必须是整型变量,argv( 第二个形参)必须是指向字符串的指针数组。加上形参说明后,main函数的函数头应写为:
int main (int argc,char *argv[]){…}或者
int main (int argc,char **argv){…}
其中第一个表示参数的个数;第二个参数中argv[0]为自身运行目录路径和程序名,argv[1]指向第一个参数、argv[2]指向第二个参数……