下图是基于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 API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
下面通过最简单的客户端/服务器程序的实例来学习socket API。
server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
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);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
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]);
write(connfd, buf, n);
close(connfd);
}
}
下面介绍程序中用到的socket API,这些函数都在sys/socket.h
中。
int socket(int family, int type, int protocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000。
int listen(int sockfd, int backlog);
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
...
close(connfd);
}
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。
client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2) {
fputs("usage: ./client message\n", stderr);
exit(1);
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 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);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
n = read(sockfd, buf, MAXLINE);
printf("Response from server:\n");
write(STDOUT_FILENO, buf, n);
close(sockfd);
return 0;
}
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
先编译运行服务器:
$ ./server
Accepting connections ...
然后在另一个终端里用netstat命令查看:
$ netstat -apn|grep 8000
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8148/server
可以看到server程序监听8000端口,IP地址还没确定下来。现在编译运行客户端:
$ ./client abcd
Response from server:
ABCD
回到server所在的终端,看看server的输出:
$ ./server
Accepting connections ...
received from 127.0.0.1 at PORT 59757
可见客户端的端口号是自动分配的。现在把客户端所连接的服务器IP改为其它主机的IP,试试两台主机的通讯。
再做一个小实验,在客户端的connect()代码之后插一个while(1);死循环,使客户端和服务器都处于连接中的状态,用netstat命令查看:
$ ./server &
[1] 8343
$ Accepting connections ...
./client abcd &
[2] 8344
$ netstat -apn|grep 8000
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8343/server
tcp 0 0 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED8344/client
tcp 0 0 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED8343/server
应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。
表 37.1. client和server的socket状态
socket文件描述符 |
源地址:源端口号 |
目的地址:目的端口号 |
状态 |
server.c中的listenfd |
0.0.0.0:8000 |
0.0.0.0:* |
LISTEN |
server.c中的connfd |
127.0.0.1:8000 |
127.0.0.1:44406 |
ESTABLISHED |
client.c中的sockfd |
127.0.0.1:44406 |
127.0.0.1:8000 |
ESTABLISHED |
上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:
#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;
}
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");
}
慢系统调用accept、read和write被信号中断时应该重试。connect虽然也会阻塞,但是被信号中断时不能立刻重试。对于accept,如果errno是ECONNABORTED,也应该重试。详细解释见参考资料。
TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用,如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回,但如果socket文件描述符有O_NONBLOCK标志,则write不阻塞,直接返回20。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,也放在wrap.c中:
ssize_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。
字段长度固定的协议往往不够灵活,难以适应新的变化。比如,以前DOS的文件名是8字节主文件名加“.”加3字节扩展名,不超过12字节,但是现代操作系统的文件名可以长得多,12字节就不够用了。那么制定一个新版本的协议规定文件名字段为256字节怎么样?这样又造成很大的浪费,因为大多数文件名都很短,需要用大量的'\0'补齐256字节,而且新版本的协议和老版本的程序无法兼容,如果已经有很多人在用老版本的程序了,会造成遵循新协议的程序与老版本程序的互操作性(Interoperability)问题。如果新版本的协议要添加新的字段,比如规定前12字节是文件名,从13到16字节是文件类型说明,从第17字节开始才是文件内容,同样会造成和老版本的程序无法兼容的问题。
现在重新看看上一节的TFTP协议是如何避免上述问题的:TFTP协议的各字段是可变长的,以'\0'为分隔符,文件名可以任意长,再看blksize等几个选项字段,TFTP协议并没有规定从第m字节到第n字节是blksize的值,而是把选项的描述信息“blksize”与它的值“512”一起做成一个可变长的字段,这样,以后添加新的选项仍然可以和老版本的程序兼容(老版本的程序只要忽略不认识的选项就行了)。
因此,常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行的比用'\0'的更常见,例如本节后面要介绍的HTTP协议。可变长字段的协议用readn来读就很不方便了,为此我们实现一个类似于fgets的readline函数,也放在wrap.c中:
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char 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;
}
1、请读者自己写出wrap.c的头文件wrap.h,后面的网络程序代码都要用到这个头文件。
2、修改server.c和client.c,添加错误处理。
目前实现的client每次运行只能从命令行读取一个字符串发给服务器,再从服务器收回来,现在我们把它改成交互式的,不断从终端接受用户输入并和server交互。
/* 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;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 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);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
编译并运行server和client,看看是否达到了你预想的结果。
$ ./client
haha1
HAHA1
haha2
the other side has been closed.
haha3
$
这时server仍在运行,但是client的运行结果并不正确。原因是什么呢?仔细查看server.c可以发现,server对每个请求只处理一次,应答后就关闭连接,client不能继续使用这个连接发送数据。但是client下次循环时又调用write发数据给server,write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错,而server收到数据后应答一个RST段,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。client下次循环又调用write发数据给server,由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序,所以看到上面的现象。
为了避免client异常退出,上面的代码应该在判断对方关闭了连接后break出循环,而不是继续write。另外,有时候代码中需要连续多次调用write,可能还来不及调用read得知对方已关闭了连接就被SIGPIPE信号终止掉了,这就需要在初始化时调用sigaction处理SIGPIPE信号,如果SIGPIPE信号没有导致进程异常退出,write返回-1并且errno为EPIPE。
另外,我们需要修改server,使它可以多次处理同一客户端的请求。
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
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);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
while (1) {
n = Read(connfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
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]);
Write(connfd, buf, n);
}
Close(connfd);
}
}
经过上面的修改后,客户端和服务器可以进行多次交互了。我们知道,服务器通常是要同时服务多个客户端的,运行上面的server和client之后,再开一个终端运行client试试,新的client能得到服务吗?想想为什么。
2.4. 使用fork并发处理多个client的请求 请点评
怎么解决这个问题?网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。
以下给出代码框架,完整的代码请读者自己完成。
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
while (1) {
connfd = accept(listenfd, ...);
n = fork();
if (n == -1) {
perror("call to fork");
exit(1);
} else if (n == 0) {
close(listenfd);
while (1) {
read(connfd, ...);
...
write(connfd, ...);
}
close(connfd);
exit(0);
} else
close(connfd);
}
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:
$ ./server
bind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
$ netstat -apn |grep 8000
tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client
tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。
现在用Ctrl-C把client也终止掉,再观察现象:
$ netstat -apn |grep 8000
tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -
$ ./server
bind error: Address already in use
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间请读者参考UNP 2.7节。
在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
有关setsockopt可以设置的其它选项请参考UNP第7章。
select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server。
/* server.c */
#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; /* save descriptor */
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客户端/服务器通讯过程。
以下是简单的UDP服务器和客户端程序。
/**//* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
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);
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;
}
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现。
编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。
问题:数组查找它和数组排序一样是重要的计算应用之一,电话公司根据姓氏查找,能容易的找到用户的电话号码和缴费情况,在学校成绩管理系统可以根据学生的学号,很容易就能查找到学生的成绩及相关资料,查找在生活中的应用是十分广泛,数据排序是一个令人感兴趣的问题,这里深入理解两种最基本的算法:线型查找和二分法查找。
线型查找:把数组的每一个元素和检索关键字比较,安顺序从第一个元素一直检索到要查找的元素,平均来说,程序要把查找关键字与一半数组元素进行比较。二分法查找:线型查找法对小型数组和未排序的数组效果较好,但是,对于大型数据来说,线型查找法效率较低。如果已经对数组排序,那么可以使用速度很快的二分法查找.
程序1:线型查找法实现对某个数的查找!
#include<stdio.h>
#include<stdlib.h>
#define Size 100
int main()
{
int linearSearch(int a[],int key,int size);
int a[Size],i,searchKey,element;
for(i=0;i<Size-1;i++)
a[i]=2*i;
printf("Enter integer search key:\n");
scanf("%d",&searchKey);
element=linearSearch(a,searchKey,Size);
if(element!=-1)
printf("Found value in element %d !\n",element);
else
printf("Value is not found!\n");
system("pause");
}
int linearSearch(int array[],int key,int size)
{
int j;
for(j=0;j<Size-1;j++)
if(array[j]==key)
return j;
return -1;
}
程序2:二分法查找法实现对某个数的查找!
#include<stdio.h>
#include<stdlib.h>
#define Size 15
int main()
{
int binarySearch(int [],int,int,int);
void printHeader(void);
void printRow(int [],int,int,int);
int a[Size],i,key,element;
for(i=0;i<=Size-1;i++)
a[i]=2*i;
printf("Enter a number between 0 and 28:");
scanf("%d",&key);
printHeader();
element=binarySearch(a,key,0,Size-1);
if(element!=-1)
printf("\n%d found in array element %d !\n",key,element);
else
printf("\n%d is not found!\n",key);
system("pause");
}
void printHeader()
{
int i;
printf("\nSubscripts:\n");
for(i=0;i<=Size-1;i++)
printf("%3d",i);
printf("\n");
for(i=1;i<=4*Size;i++)
printf("-");
printf("\n");
}
int binarySearch(int array[],int searchKey,int low,int high)
{
void printRow(int array[],int low,int middle,int high);
int middle;
while(low<=high)
{
middle=(low+high)/2;
printRow(array,low,middle,high);
if(searchKey==array[middle])
return middle;
else if(searchKey<array[middle])
high=middle-1;
else
low=middle+1;
}
return -1;
}
void printRow(int array[],int low,int middle,int high)
{
int i;
for(i=0;i<=Size-1;i++)
if(i<low||i>high)
printf(" ");
else if(i==middle)
printf("%3d*",array[i]);
else
printf("%3d",array[i]);
printf("\n");
}
效率分析:线型查找摆脱了数组排序的约束,不足之处是不适合大型数据查找,并且查找方法比较老套,如果要找的数在数组中最后一个数n,那么搜索从0开始,一直检索到n,要经过n次遍历,时间复杂度:O(n),而二分查找法中如果查找关键字小于数组中间的元素,就查找数组的头半部分,否则查找数组的后半部分,时间复杂度:O(log2n),如果在指定子数组中还没有查找到关键字,就再把子数组折半,反复进行这种查找,直到要查找的关键字等于子数组中间的元素,或没有找到关键字为止。在最坏的情况下,用二分法查找有1024个元素的数组也只需要比较10次,即用2除1024,连续除10次得到1为止,如果有1048576(2的20次方)个元素,用二分法只要比较20次就可以找到要查找的元素,而用简单的线型查找则需要进行2的20次方查找,可见二分法比线型查找法的效率要高得多,对10亿哥元素的数组来说,平均比较5亿次和30次简直是天壤之别!所以掌握二分法对在庞大的数组库处理是很有效的!
问题:数组排序(即按某种特定的顺序排列数据,如升序或降序)是最重要的计算应用之一,银行用帐号对所有的支票进行能够排序,并根据排序结果准备月底的财务报告,学校学生成绩管理系统用数组排序的方法将考试成绩从高到低进行排名,数组排序方法很多,有直接插入排序、冒泡排序、快速排序、直接选择排序,下面来详细介绍这四种基本的排序方法及其实现。
1,直接插入排序:当数据表A中每个元素距其最终位置不远,数据表A按关键字值基本有序,可用此方法排序较快。
2,冒泡排序法:将较小的值“上浮”到数组顶部,而较大值“下沉”到数组底部,这种排序技术要比较好几趟,每一趟要比较连续的数组元素对,如果某对数值是按升序排序的(或者这两个值相等),那就保持原样,如果某对数组是按降序排列的,就要交换它们的值。
3,快速排序法:快速排序是对冒泡排序的一种改进。它的基本思想是:通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按次方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
4,直接选择排序法:直接选择排序的作法是:第一趟扫描所有数据,选择其中最小的一个与第一个数据互换;第二趟从第二个数据开始向后扫描,选择最小的与第二个数据互换;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。它比起冒泡排序有一个优点就是不用不断的交换。
程序1:直接插入法实现对数组的排序!
#include<stdio.h>
#include<conio.h>
int main()
{
void InsertSort(int [],int);
int a[7]={8,10,2,3,1,7,13};
int i;
InsertSort(a,7);
for(i=0;i<7;i++)
printf("%4d",a[i]);
getch();
}
void InsertSort(int a[],int count)
{
int i,j,temp;
for(i=1;i<count;i++)
{
temp=a[i];
j=i-1;
while(a[j]>temp && j>=0)
{
a[j+1]=a[j];
j--;
}
if(j!=(i-1))
a[j+1]=temp;
}
}
程序2:冒泡法实现对数组的排序!
#include<stdio.h>
#include<conio.h>
int main()
{
void BubbleSort(int []);
int a[10];
int i,j,temp;
printf("Input tem integer numbers for a[10]:");
for(i=0;i<10;i++)
scanf("%d,",&a[i]);
printf("\n");
BubbleSort(a);
printf("The sorted array is:\n");
for(j=0;j<10;j++)
printf("%d,",a[j]);
printf("\n\n");
getch();
}
void BubbleSort(int array[])
{
int i,j,temp;
for(j=0;j<9;j++)
for(i=0;i<9-j;i++)
if(array[i]>array[i+1])
{
temp=array[i];
array[i]=array[i+1];
array[i+1]=temp;
}
}
程序3:快速排序法实现对数组的排序!
#include<stdio.h>
#include<conio.h>
#define Max 8
int main()
{
void QuickSort(int a[],int p,int r);
int a[]={2,8,7,1,3,5,6,4};
QuickSort(a,1,Max);
printf(" The sorted array is :");
for(int i=0;i<Max;i++)
printf("%d,",a[i]);
printf("\n");
getch();
}
void QuickSort(int a[],int p,int r)
{
int Partition(int a[],int p,int r);
if(p<r)
{
int q=Partition(a,p,r);
QuickSort(a,p,q-1);
QuickSort(a,q+1,r);
}
}
int Partition(int a[],int p,int r)
{
int i=p-1;
int x=a[r-1];
for(int j=p;j<r;j++)
{
if(a[j-1]<=x)
{
i=i+1;
int temp;
temp=a[j-1];
a[j-1]=a[i-1];
a[i-1]=temp;
}
}
int temp;
temp=a[i];
a[i]=a[r-1];
a[r-1]=temp;
return i+1;
}
程序4:直接选择法实现对数组的排序!
#include<stdio.h>
#include<conio.h>
int main()
{
void ChooseSort(int []);
int i,j,a[10];
printf("Input ten integer numbers for a[10]: ");
for(i=0;i<10;i++)
scanf("%d,",&a[i]);
printf("\n");
ChooseSort(a);
printf("The sorted array is:\n");
for(j=0;j<10;j++)
printf("%d,",a[j]);
printf("\n\n");
getch();
}
void ChooseSort(int array[])
{
int j,temp,*p1,*p2;
for(p1=array;p1<array+9;p1++)
{
j++;
for(p2=array+j;p2<=array+9;p2++)
if(*p2<*p1)
{
temp=*p2;
*p2=*p1;
*p1=temp;
}
}
}
各种排序方法的综合比较:
一、时间性能
按平均的时间性能来分,四种类排序方法时间复杂度分别为:
直接插入排序法:O(n^2),冒泡排序法:O(n^2)快速排序法:O(nlogn),直接选择排序法:O(n^2)
时间复杂度为O(n^2)的有:直接插入排序、起泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;当待排记录序列按关键字顺序有序时,直接插入排序和起泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能蜕化为O(n2),因此是应该尽量避免的情况。
二、排序方法的稳定性能
1. 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。
2. 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
3. 对于不稳定的排序方法,只要能举出一个实例说明即可。
4. 快速排序是不稳定的排序方法。
今天在准备发布用VS2005写的那个程序时,拷贝到我同事机器上,双击突然出现了“由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题“,这个问题很让我意外,以前只出现过缺少DLL的情况,而这次出现这个问题,让我一时没办法。想想,无非是两个原因引起的,要么是他没有安装VS2005的原因,要么是我的程序里依赖了其它的一些库。于是百度一下,发现好多相关主题。我是按照这个帖子解决的:
在VS2005下用C++写的程序,在一台未安装VS2005的系统上,
用命令行方式运行,提示:
“系统无法执行指定的程序”
直接双击运行,提示:
“由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题”
以前用VC6和VS2003的话, 如果缺少库文件,是会提示缺少“**.dll”,但是用VS2005却没有这样的提示。
自己实验了一下,感觉以下几种解决办法是可行的:
方法一:
在类似C:\Program Files\Microsoft Visual Studio 8\VC\redi
st\Debug_NonRedist\x86\Microsoft.VC80.DebugCRT 下找到了下列文件:
msvcm80d.dll
msvcp80d.dll
msvcr80d.dll
Microsoft.VC80.DebugCRT.manifest
把这几个文件拷贝到目标机器上,与运行程序同一文件夹或放到system32下,就可以正确运行了。
其他release版、MFC程序什么的都是拷redist下相应文件夹下的文件就可以了,文件夹后都有标识!
方法二:
修改编译选项,将/MD或/MDd 改为 /MT或/MTd,这样就实现了对VC运行时库的静态链接,在运行时就不再需要VC的dll了。
方法三:
工程-》属性-》配置属性-》常规-》MFC的使用,选择“在静态库中使用mfc”
这样生成的exe文件应该就可以在其他机器上跑了。
方法四:
你的vc8安装盘上找到再分发包vcredist_xxx.exe和你的程序捆绑安装
C#调用c++制作的DLL时,一些参数的赋值问题如char *,结构体
c++ dll中的原型
int test(char* xm,char* fa,UINT &VerNum,double Mile,char *SurvMile);
c#调用时
[DllImport(@"Test2.DLL")]
public static extern int test(string xm,string fa,ref UInt32 VerNum,double Mile, StringBuilder SurvMile);
注意:
1.调用的时候,有部分char* ,如果想获得返回值,不能用string 作参数来进行调用,这样得不到返回到结果,可以用StringBuilder来声明变
StringBuilder strMyTemp = new StringBuilder(256);//256是长度
2.结构体的引用传递
首先在c#中定义和c++相同的结构体,如果是引用传递,在结构体前面加上[In, Out]
[DllImport(@"test.dll")]
public static extern int test([In, Out] SLineData[] lndt,ref UInt32 length);
3.其他的类型如整形等等用ref加上数据变量则可获得返回值
使用C++调用C#的DLL
SwfDotNet是C#编写的,作者的C#水平,真是令我佩服。这是个特别好的读写Swf文件的库。但是,我要用在C++项目中,怎么让C++调用C#的DLL呢。今天一上午都在琢磨这个问题,耽误了很多时间,原因是编译是出现:
warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失。
接着就是一大堆的0x01等等。自己做了个Sample,仔细分析发现还是自己没有搞清楚。正确的操作如下:
1 创建C# DLL,需要指定应用类型为“类库”,代码:
namespace CSLib
{
public class Class1
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = "Your Name: " + value;
}
}
}
}
2 C++客户程序,是个控制台应用,代码:
#using "..\debug\CSLib.dll"
using namespace CSLib;
int _tmain(int argc, _TCHAR* argv[])
{
Class1 ^c = gcnew Class1();
c->Name = "zzj";
printf("%s\n", c->Name);
return 0;
}
3 几点要记住:
1 使用#using引用C# DLL,而不是#include。我就是想当然的使用了后者,所以浪费了一上午的时间;
2 别忘了using namespace CSLib;
3 使用C++/clr语法,采用正确的访问托管对象,即:使用帽子'^',而不是星星'*'(选择菜单[项目]->[属性],在其[属性页]中的[公共语言运行库支持]项)
1、去掉Oracle生成的SQL创建语句中的双引号
用powerdesigner导出orale数据库的建表sql时,默认会给表名和字段名加上双引号,如下图:
这样给操作数据库带来很大的不便,解决的办法是设置Database菜单,
然后点击Edit Current DBMS菜单,再依次点开Script->Format,然后找到CaseSensitivityUsingQuote
将其设为NO,即可。如下图:
如果带有包的话,导出时要选择包中的表。
2、PowerDesign高级应用
编写相关的VBS脚本在PowerDesign里自定义一些命令与操作等,具体的可以参考C:\Program Files\Sybase\PowerDesigner 9\VB Scripts目录下的脚本示例。怎么运用这些脚本呢?
在Tools->Execute Commands里可以进行操作。具体说明在帮助里写的很清楚。帮助的位置在 PowerDesigner General Features Guide-> PART 2. Modeling Guide->CHAPTER 8. Managing Objects->Accessing objects using VBScript->VBScript uses in PowerDesigner
PowerDesign的使用主要是DBMS的配置
3、修改建表脚本生成规则。
如果每个表格都有相同的字段,可以如下修改:
Database -> Edit Current DBMS 展开 Script -> Object -> Table -> Create 见右下的Value值,可以直接修改如下:
/* tablename: %TNAME% */
create table [%QUALIFIER%]%TABLE% (
%TABLDEFN%
ts char(19) null default convert(char(19),getdate(),20),
dr smallint null default 0
)
[%OPTIONS%]
其中的 ts、dr 两列会在生成SQL脚本的时候自动的插入每个表格中,其中的%TNAME% 变量是给每个表格的SQL添加一个该表的Name值注释。
4、修改字段生成规则。
要给每个字段都添加一个注释的话,同一窗口中展开 Script -> Object -> Column -> Add 的 Value修改为:
%20:COLUMN% [%COMPUTE%?AS (%COMPUTE%):%20:DATATYPE% [%IDENTITY%?%IDENTITY%:[%NULL%][%NOTNULL%]][ default %DEFAULT%]
[[constraint %CONSTNAME%] check (%CONSTRAINT%)]]/*%COLNNAME%*/
其中的%COLNNAME%就是列的Name值(可以是中文)
5、修改外键命名规则。
选择Database—>Edit Current DBMS
选择Scripts-》Objects-》Reference-》ConstName
可以发现右侧的Value为:
FK_%.U8:CHILD%_%.U9:REFR%_%.U8:PARENT%
可见,该命名方法是:'FK_'+8位子表名+9位Reference名+8位父表名,你可以根据这中模式自定义为:
FK_%.U7:CHILD%_RELATIONS_%.U7:PARENT%,
可以使FK名称变为FK_TABLE_2_RELATIONS_TABLE_1
掌握这种方法后就可以按照自己的想法修改了
生成建库脚本SQL文件中的表头注释很讨厌,可以在 Databse -> Generate Database (Ctrl+G)窗口中,选择Options卡片,去掉Usage的Title钩选项即可。
6、添加外键
Model -> References新建一条外键后,双击进入外键属性,在“Joins”卡片中可以选择子表的外键字段。如下图:
接着出现如下画面:
按照步骤操作即可。
7、取消name和code联动
在修改name的时候,code的值将跟着变动,很不方便。修改方法:PowerDesign中的选项菜单里修改,在[Tool]-->[General Options]->[Dialog]->[Operating modes]->[Name to Code mirroring],这里默认是让名称和代码同步,将前面的复选框去掉就行了。如图:
编写相关的VBS脚本在PowerDesign里自定义一些命令与操作等,具体的可以参考C:\Program Files\Sybase\PowerDesigner 9\VB Scripts目录下的脚本示例。怎么运用这些脚本呢?
在Tools-》Execute Commands里可以进行操作。具体说明在帮助里写的很清楚。帮助的位置在 PowerDesigner General Features Guide-> PART 2. Modeling Guide->CHAPTER 8. Managing Objects->Accessing objects using VBScript->VBScript uses in PowerDesigner
PowerDesign的使用主要是DBMS的配置
1、修改建表脚本生成规则。如果每个表格都有相同的字段,可以如下修改:
Database -> Edit Current DBMS 展开 Script -> Object -> Table -> Create 见右下的Value值,可以直接修改如下:
/* tablename: %TNAME% */
create table [%QUALIFIER%]%TABLE% (
%TABLDEFN%
ts char(19) null default convert(char(19),getdate(),20),
dr smallint null default 0
)
[%OPTIONS%]
其中的 ts、dr 两列会在生成SQL脚本的时候自动的插入每个表格中,其中的%TNAME% 变量是给每个表格的SQL添加一个该表的Name值注释。
2、修改字段生成规则。要给每个字段都添加一个注释的话,同一窗口中展开 Script -> Object -> Column -> Add 的 Value修改为:
%20:COLUMN% [%COMPUTE%?AS (%COMPUTE%):%20:DATATYPE% [%IDENTITY%?%IDENTITY%:[%NULL%][%NOTNULL%]][ default %DEFAULT%]
[[constraint %CONSTNAME%] check (%CONSTRAINT%)]]/*%COLNNAME%*/
其中的%COLNNAME%就是列的Name值(可以是中文)
3、修改外键命名规则。选择Database—>Edit Current DBMS
选择Scripts-》Objects-》Reference-》ConstName
可以发现右侧的Value为:
FK_%.U8:CHILD%_%.U9:REFR%_%.U8:PARENT%
可见,该命名方法是:'FK_'+8位子表名+9位Reference名+8位父表名,你可以根据这中模式自定义为:
FK_%.U7:CHILD%_RELATIONS_%.U7:PARENT%,
可以使FK名称变为FK_TABLE_2_RELATIONS_TABLE_1
掌握这种方法后就可以按照自己的想法修改了
生成建库脚本SQL文件中的表头注释很讨厌,可以在 Databse -> Generate Database (Ctrl+G)窗口中,选择Options卡片,去掉Usage的Title钩选项即可。
4、添加外键
Model -> References新建一条外键后,双击进入外键属性,在“Joins”卡片中可以选择子表的外键字段
5、去掉生成的SQL脚本双引号的问题:ORACLE 8I2::Script\Sql\Format\CaseSensitivityUsingQuote改成No,默认是Yes所以会有双引号。
在修改name的时候,code的值将跟着变动,很不方便。修改方法:PowerDesign中的选项菜单里修改,在[Tool]-->[General Options]->[Dialog]->[Operating modes]->[Name to Code mirroring],这里默认是让名称和代码同步,将前面的复选框去掉就行了。