网络编程学习笔记(一)理解网络编程和套接字

  因为需要开始学习网络编程,因为之前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,失败时返回非零的代码值
  1. Winsock中存在多个版本,应准备WORD类型的(WORD是通过typedef生命定义的unsingned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested。如版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。
    我们可以借助MAKEWORD宏函数来构建WORD型版本信息。
    例如1.2我们可以用MAKEWORD(1, 2);
  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]指向第二个参数……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值